Accepted
2025-12-17
Sensitive data (API tokens, passwords, database credentials) is currently stored as plain String types throughout the codebase. This creates several security and maintainability issues:
-
Accidental Exposure: Secrets appear in full when printed with
Debugformatting, potentially leaking through:- Debug logs during development
- Error messages and stack traces
- Panic outputs
- Test output and CI logs
-
No Type-Level Security: The type system doesn't distinguish between secrets and regular strings:
- No compile-time guarantee that secrets are handled carefully
- Easy to accidentally log or print secret values
- No centralized place to add security enhancements
-
Difficult Auditing: Hard to track secret usage:
- Can't easily find all places where secrets are used
- Can't grep for secret-specific types
- No visibility into when/where secrets are exposed
-
Memory Security Gap: Secrets remain in memory even after being dropped, potentially accessible through:
- Memory dumps
- Core dumps
- Swap files
- Process memory inspection
// Current problematic approach
#[derive(Debug)]
pub struct HetznerConfig {
pub api_token: String, // Exposed in debug output!
}
// This accidentally logs the token
tracing::debug!("Config: {:?}", config);
// Output: Config: HetznerConfig { api_token: "hf_abc123..." }
pub struct MysqlConfig {
pub password: String, // Visible in error messages!
}
// Error contains password
return Err(format!("Failed to connect to {:?}", config));- Identification: Clearly identify where secrets are used in the codebase
- Redacted Output: Prevent accidental exposure through debug/display formatting
- Memory Security: Wipe secrets from memory when no longer needed
- Maintainability: Keep solution simple and well-documented
- Standards Compliance: Follow Rust ecosystem best practices
Adopt the secrecy crate (https://crates.io/crates/secrecy) as the standard solution for handling sensitive data throughout the codebase.
Core Type: Secret<T> from the secrecy crate
// src/shared/secret.rs
pub use secrecy::{ExposeSecret, Secret, SecretString};
use secrecy::SerializableSecret;
// Enable serialization for String secrets (required for config files)
impl SerializableSecret for String {}
// Domain-specific type aliases for clarity
pub type ApiToken = Secret<String>;
pub type Password = Secret<String>;Usage Pattern:
use crate::shared::{ApiToken, ExposeSecret};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HetznerConfig {
pub api_token: ApiToken, // Automatically redacted in Debug
// ...
}
// Creating with secret
let config = HetznerConfig {
api_token: Secret::new("token".to_string()),
};
// Debug output is safe
println!("{:?}", config); // HetznerConfig { api_token: Secret([REDACTED]) }
// Explicit exposure when needed
let token_str = config.api_token.expose_secret();Locations to Apply:
-
Provider Secrets:
HetznerConfig.api_token→ApiToken
-
Database Secrets:
MysqlConfig.password→Password
-
API Secrets:
HttpApiSection.admin_token→ApiTokenHttpApiConfig.admin_token→ApiToken
-
Battle-Tested Security:
- Used by major Rust projects (diesel, sqlx, etc.)
- Audited and maintained by security-conscious community
- Implements best practices from cryptography experts
-
Memory Zeroing:
- Uses
zeroizecrate to securely wipe memory on drop - Prevents secrets from lingering in memory/swap/core dumps
- Hard to implement correctly in custom solution
- Uses
-
Industry Standard:
- De facto standard for secret handling in Rust
- Well-documented with extensive examples
- Future maintainers will recognize the pattern
-
Minimal Complexity:
- Single dependency (
secrecy+ transitivezeroize) - Simple API:
Secret::new()andexpose_secret() - Type aliases reduce boilerplate
- Single dependency (
-
Future-Proof:
- If security requirements evolve (audits, compliance), infrastructure is ready
- Can easily add more advanced features if needed
- No need to retrofit memory zeroing later
Pros of Custom:
- Zero dependencies
- Full control
- Simpler initial implementation
Cons of Custom (Why we rejected it):
- ❌ No memory zeroing (significant security gap)
- ❌ Need to implement everything ourselves
- ❌ Risk of security mistakes in implementation
- ❌ Reinventing a well-solved problem
- ❌ Future maintainers less likely to understand custom approach
A custom wrapper around secrecy would add:
- Extra abstraction layers
- More code to maintain
- Learning curve for contributors
- No significant benefits over direct usage
The secrecy API is already simple and well-designed - wrapping it adds unnecessary complexity.
✅ Security Improvements:
- Secrets automatically redacted in debug output
- Memory securely wiped on drop (via
zeroize) - Type-safe secret handling at compile time
- Industry-standard security practices
✅ Code Quality:
- Clear identification of all secret values (grep for
Secret<T>) - Explicit
expose_secret()calls visible in code review - Type aliases improve readability (
ApiTokenvsString) - Consistent pattern across codebase
✅ Maintainability:
- Standard solution recognized by Rust developers
- Extensive documentation and examples available
- Community support and updates
- Easy to audit secret usage
✅ Minimal Overhead:
- Small dependency (single crate + zeroize)
- No runtime performance impact
no_stdcompatible (if we ever need it)- Well-maintained and stable
- Contributors need to learn
expose_secret()pattern - Must implement
SerializableSecretmarker trait per type - Slightly more verbose than plain
String
Mitigation: Add examples to AGENTS.md and create comprehensive ADR (this document).
- Need
impl SerializableSecret for String {}once - Intentional friction to prevent accidental serialization
Mitigation: Single implementation covers all Secret<String> uses.
- Adds
secrecy(~50KB) andzeroize(~20KB) to dependency tree
Mitigation: Tiny, stable dependencies with strong security track record.
Affected Modules (requires updates):
src/domain/provider/hetzner.rs(API token)src/domain/tracker/database/mysql.rs(password)src/application/command_handlers/create/config/tracker/http_api_section.rs(admin token)src/domain/tracker/http_api.rs(admin token)- All tests using these types
Breaking Changes: None (internal refactoring only)
Timeline: Estimated 2-3 sprints for complete migration
// Custom implementation
pub struct Secret<T> {
inner: T,
}
impl<T> Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Secret([REDACTED])")
}
}Rejected because:
- No memory zeroing on drop (major security gap)
- Would need to add
zeroizedependency anyway - Reinventing already-solved problem
- More code to maintain and audit
- Less trustworthy than community-vetted solution
Use comments and documentation to mark secret fields:
pub struct Config {
/// SECRET: Never log this field
pub api_token: String,
}Rejected because:
- No type-safety or compile-time guarantees
- Easy to accidentally violate conventions
- No automatic redaction of debug output
- No memory security
- Impossible to audit automatically
Alternative crate with more advanced features (mlock, mprotect).
Rejected because:
- Requires
stdandlibc(notno_stdcompatible) - Heavier dependency
- More complexity than we currently need
secrecyis more widely adopted
- Error Context Strategy - Errors must not expose secret values
- Actionable Error Messages - Error messages must redact secrets
- Development Principles - Security and observability principles
- Secrecy Crate: https://docs.rs/secrecy/latest/secrecy/
- Zeroize Crate: https://docs.rs/zeroize/latest/zeroize/
- Security Best Practices: https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/
- Rust API Guidelines: https://rust-lang.github.io/api-guidelines/
- Related Issue: Secret Type Introduction Refactor Plan
- Add
secrecydependency toCargo.toml - Create
src/shared/secret.rsmodule - Export types and implement
SerializableSecretforString - Add type aliases:
ApiToken,Password,SecretString
- Update
HetznerConfig.api_tokento useApiToken - Update all Hetzner-related tests
- Verify no secrets in debug output
- Update
MysqlConfig.passwordto usePassword - Update all MySQL-related tests
- Verify template rendering works correctly
- Update
HttpApiSection.admin_tokento useApiToken - Update
HttpApiConfig.admin_tokento useApiToken - Update all HTTP API tests
- Update
AGENTS.mdwith secret handling rule - Add examples to module documentation
- Update contributing guidelines
For each phase, verify:
- ✅ All unit tests pass
- ✅ Debug output shows
[REDACTED]instead of actual values - ✅ Serialization/deserialization works correctly
- ✅ Error messages don't expose secrets
- ✅ All linters pass (clippy, rustfmt, etc.)
Last Updated: 2025-12-17