The secrecy crate wraps a value so reading it requires an explicit ExposeSecret call, which is easy to audit, and wipes it from memory on drop using zeroize. It helps stop accidental leaks through debug logging. It does not provide mlock or mprotect style memory protection, so it is hygiene, not a vault.
Definition
A secret in a Rust service might be a database password, an API key or a signing key. If it sits in a plain String, a stray debug print, a derived Debug on a config struct or a panic message can write it into logs. The secrecy crate gives you wrapper types, such as SecretBox and a string alias, that make that harder.
What the documentation states
From the crate documentation:
- Access to the value goes through the ExposeSecret and ExposeSecretMut traits, described as making access "explicit and easy-to-audit".
- The goal is to prevent accidental leakage "via channels like debug logging". Check the implementation of the types you use to confirm how Debug output is shown.
- Values are wiped from memory on drop through the zeroize crate.
- It "does not provide more advanced memory protection mechanisms e.g. ones based on mlock(2)/mprotect(2)".
- By default SecretBox has no Serialize implementation, which blocks exfiltration through serde. Serialization needs an explicit opt in through the SerializableSecret marker trait.
- When loading secrets through serde, intermediate copies such as unparsed input must be cleaned up manually.
What this means in practice
| Risk | Does the crate help? |
|---|---|
| Secret printed by a Debug derive in a log line | Yes, if the field is wrapped |
| Secret serialized into a JSON response by accident | Yes, since serialization is opt in |
| Secret left in freed memory | Partly, zeroize on drop clears the wrapped value |
| Copies of the secret in the original input buffer | No, you must clear those |
| Attacker with memory read access to the process | No |
| Secret exposed through environment variables or process listings | No |
The last rows matter most. A wrapper type does not change where a secret came from or who can read it. Environment variables are visible to other processes in some setups and may appear in crash dumps. OWASP's secrets guidance recommends mounted files or in memory injection from a secret store when possible, plus rotation and least privilege.
Example
use secrecy::{ExposeSecret, SecretString};
struct DbConfig {
host: String,
password: SecretString,
}
fn connect(cfg: &DbConfig) {
let pw = cfg.password.expose_secret();
// pass pw to the driver, do not log it
}
Search a codebase for expose_secret and review each call. That short list is the audit surface the crate is designed to create.
Steps
- Wrap every secret field in a secret type in your config structs.
- Keep Debug and Serialize off structs that carry secrets unless the fields are wrapped.
- Review each expose_secret call site and make sure the value is not logged.
- Clear or avoid long lived copies of raw input such as config file contents.
- Load secrets from a managed store or mounted file, and rotate them.
- Run a secret scan on the repository to catch values committed by mistake.
Try it yourself
Click through it at your own pace, or open it full screen. Look for how hardcoded credentials in source are surfaced, which wrapper types alone do not prevent.
Quick check
Does zeroize on drop make a leaked secret safe?
No. It clears memory when the value is dropped. It does nothing about secrets already logged, committed or sent elsewhere. Rotate any secret that has leaked.
Should I use this instead of a secrets manager?
No. It protects how the value is handled in your process. A secrets manager controls storage, access and rotation.
Safeguard's dependency scanning and SBOM generation read Rust lock files, so the crates you ship are inventoried and checked against advisories, and the free scan is at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.