Rust SDK
SecretSpec provides a Rust library with type-safe access to secrets through a
derive macro. The macro reads secretspec.toml at compile time and generates
Rust types for its profiles and secrets.
Quick start
Section titled “Quick start”Add the runtime, derive macro, and the generated code’s direct dependencies to
your Cargo.toml:
[dependencies]secretspec = "0.18"secretspec-derive = "0.18"secrecy = { version = "0.10", features = ["serde"] }serde = { version = "1", features = ["derive"] }The examples on this page are compiled as Cargo examples in
secretspec-derive. They generate their types from this manifest:
[project]
name = "rust-sdk-example"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string", required = true }
REDIS_URL = { description = "Redis connection string", required = false }
TLS_CERT = { description = "TLS certificate", required = true, as_path = true }
TLS_KEY = { description = "TLS private key", required = false, as_path = true }
[profiles.development]
DATABASE_URL = { default = "postgresql://localhost/development" }
[profiles.production]
DATABASE_URL = { required = true }
API_KEY = { description = "Production API key", required = true }
[scopes.api]
secrets = ["DATABASE_URL"]
declare_secrets! generates SecretSpec, Profile, and
SecretSpecProfile. The standard loader returns the union type that is safe to
use with any declared profile:
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let resolved = SecretSpec::builder()
.with_provider("keyring://")
.with_profile("development")
.with_reason("start application")
.load()?;
println!("Database: {}", resolved.secrets.database_url);
if let Some(redis_url) = &resolved.secrets.redis_url {
println!("Redis: {redis_url}");
}
resolved.secrets.set_as_env_vars();
println!("Profile: {}", resolved.profile);
println!("Provider: {}", resolved.provider);
Ok(())
}
Required and defaulted secrets are generated as String; secrets that may be
absent are Option<String>. Field names use Rust snake case, so DATABASE_URL
becomes database_url.
Profile-specific types
Section titled “Profile-specific types”Use load_profile() when code should receive the exact shape of the selected
profile. It returns a SecretSpecProfile enum whose variants contain that
profile’s effective fields, including fields inherited from
[profiles.default]:
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let resolved = SecretSpec::builder()
.with_provider("keyring://")
.with_profile(Profile::Production)
.with_reason("start production application")
.load_profile()?;
match resolved.secrets {
SecretSpecProfile::Production {
database_url,
api_key,
..
} => {
println!("Database: {database_url}");
println!("API key loaded: {} bytes", api_key.len());
}
_ => unreachable!("the production profile was selected"),
}
Ok(())
}
Scopes (0.17+)
Section titled “Scopes (0.17+)”A scope resolves only a named subset of a profile. Scopes
are available through the untyped Secrets API:
use secretspec::Secrets;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut spec = Secrets::load()?;
spec.set_scope("api");
let resolved = spec.resolve()?;
assert_eq!(resolved.scope.as_deref(), Some("api"));
Ok(())
}
resolve() and report() both return the active scope. The untyped API also
honors SECRETSPEC_SCOPE when no scope is selected explicitly.
Typed loaders generated by declare_secrets! deliberately do not support
scopes. A generated struct has a field for every declared secret, so hiding one
would leave that field unfillable. SecretSpec::builder() therefore has no
with_scope, and typed load() and load_profile() always resolve the full
profile. Use a separate manifest or the untyped API when a component needs a
narrowed set.
Secrets as file paths
Section titled “Secrets as file paths”Secrets declared with as_path = true are generated as PathBuf instead of
String. Optional file-shaped secrets use Option<PathBuf>:
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let resolved = SecretSpec::builder()
.with_provider("keyring://")
.with_reason("configure TLS")
.load()?;
let certificate: &std::path::PathBuf = &resolved.secrets.tls_cert;
println!("Certificate: {}", certificate.display());
if let Some(private_key) = &resolved.secrets.tls_key {
println!("Private key: {}", private_key.display());
}
// The materialized files remain valid until `resolved` is dropped.
Ok(())
}