secrets-rs
A Rust library for safely retrieving and using secrets in applications, primarily for configuration.
The core guarantee: a secret's real value must be explicitly requested. Every default access path — Display, Debug, and serde serialization — emits a masked value that is safe to include in logs and error reports.
Concepts
Secret
Secret<T> is a generic wrapper around a typed value. The supported types are:
| Type | T |
|---|---|
| UTF-8 string | String |
| Raw bytes | Vec<u8> |
| JSON | serde_json::Value |
A secret is identified by a URN of the form:
urn:secrets-rs:<source_id>:<name>
The scheme (urn) and NID (secrets-rs) are case-insensitive per RFC 8141. The case sensitivity of source_id and name depends on the source.
Masked value
Until a secret is bound, or whenever it is displayed by default, it shows a masked value:
urn:secrets-rs:env:MY_API_KEY [UNBOUND] # before binding
urn:secrets-rs:env:MY_API_KEY [string:22] # after binding
The format is <urn> [<type>:<size>]. Calling .value() before binding returns an error.
Sources
A source is anything that can look up a secret by name and return its raw bytes. Sources are registered in a SourceRegistry keyed by the source_id from the URN.
Built-in source:
| Source | source_id convention |
Backed by |
|---|---|---|
EnvSource |
any string (e.g. "env") |
std::env::var |
Binding
Binding resolves a secret from its source and stores the typed value inside the Secret<T> struct. You can bind secrets individually with Secret::bind, or bind every secret in a struct at once with bind_all.
Usage
Add the dependency
[]
= { = "..." } # or version once published
Individual binding
use ;
let mut api_key: =
new?;
let mut registry = new;
registry.register;
api_key.bind?;
// Safe to log — shows the masked value
println!;
// Explicit opt-in to the real value
let key: &str = api_key.value?;
Config struct with #[derive(Bindable)]
For structs that contain multiple secrets, derive Bindable to generate bind_all support automatically. Non-Secret fields are ignored.
use ;
let mut config = AppConfig ;
let mut registry = new;
registry.register;
// Binds db_password and api_key; collects all errors rather than
// stopping at the first failure.
bind_all?;
Without the derive macro, implement Bindable manually:
use ;
Serde integration
Secret<T> implements both Serialize and Deserialize:
- Serialize — always produces the masked value string, safe to use in any context.
- Deserialize — accepts a
urn:secrets-rs:<source_id>:<name>string and produces an unbound secret. Non-URN strings are rejected with an error.
This means a config file can hold URN strings and be deserialized directly into a typed struct; bind_all is then called to resolve the actual values from their sources.
// Deserialize URNs from config file, then bind to real values
let mut config: AppConfig = from_str?;
bind_all?;
// Serializes as "urn:secrets-rs:env:DB_PASSWORD [string:28]"
println!;
Examples
Runnable examples are in the examples/ directory:
Out of scope
- Writing secrets back to sources
- In-memory encryption