Secrets Management
Extensions that access external services (HTTP APIs, databases, cloud storage)
commonly need credentials. DuckDB has a native secrets system (CREATE SECRET),
but the extension C API exposes none of it: duckdb_ext_api_v1 has no
duckdb_secret_* functions, so an extension cannot ask DuckDB for a credential.
The secrets
module therefore offers two separate things:
list_duckdb_secretsreads the metadata of the secrets the user has configured, with sensitive fields redacted by DuckDB.SecretsManagerandSecretEntryare a trait and a type for the credential source an extension has to provide itself (an environment variable, a config option, a file, its own key store), with leak-resistant defaults already in place.
Listing DuckDB's secrets
list_duckdb_secrets queries the duckdb_secrets() table function and returns
one DuckDbSecretInfo per secret: name, secret_type, provider,
persistent, storage, scope (the URI prefixes it applies to) and
secret_string. DuckDB redacts sensitive fields in that table, so a secret
created with SECRET 'super-secret-value' comes back as ...;secret=redacted.
Use it to find out which secrets exist and what they cover, not to
authenticate:
#![allow(unused)] fn main() { use quack_rs::secrets::list_duckdb_secrets; use libduckdb_sys::duckdb_connection; unsafe fn demo(con: duckdb_connection) -> Result<(), quack_rs::error::ExtensionError> { // SAFETY: `con` is a valid, open connection. let secrets = unsafe { list_duckdb_secrets(con) }?; if !secrets.iter().any(|s| s.secret_type == "s3") { eprintln!("no S3 secret configured; run CREATE SECRET (TYPE s3, ...)"); } Ok(()) } }
Core Types
SecretEntry
A single secret entry with metadata and key-value fields. Designed to minimize accidental credential leakage:
Debugredacts field values and the scope — field keys are shown, and every value and a non-empty scope are replaced with"[REDACTED]"Dropzeroizes sensitive data — every field key and value, the provider and the scope are overwritten with zeros usingstd::ptr::write_volatilebefore deallocation. This covers the buffers aSecretEntryowns, not aStringthe caller passed in and still holds- No
PartialEq— prevents accidental non-constant-time comparisons of secret material Cloneduplicates the secret — it is supported, but each clone is another copy of the credential in memory
SecretsManager
The trait an extension implements over its own credential source. It requires
Send + Sync:
#![allow(unused)] fn main() { use quack_rs::secrets::{SecretEntry, SecretsManager}; struct MySecrets { entries: Vec<SecretEntry>, } impl SecretsManager for MySecrets { fn get_secret(&self, name: &str, secret_type: &str) -> Option<SecretEntry> { self.entries.iter() .find(|e| e.name() == name && e.secret_type() == secret_type) .cloned() } fn list_secrets(&self, secret_type: Option<&str>) -> Vec<SecretEntry> { self.entries.iter() .filter(|e| secret_type.is_none() || secret_type == Some(e.secret_type())) .cloned() .collect() } fn remove_secret(&self, _name: &str, _secret_type: &str) -> bool { false // read-only example } } }
Building Secret Entries
Use the builder pattern:
#![allow(unused)] fn main() { use quack_rs::secrets::SecretEntry; let entry = SecretEntry::new("my_api_key", "bearer") .with_provider("config") .with_scope("https://api.example.com") .with_field("token", "sk-abc123") .with_field("refresh_token", "xyz789"); assert_eq!(entry.name(), "my_api_key"); assert_eq!(entry.secret_type(), "bearer"); assert_eq!(entry.get_field("token"), Some("sk-abc123")); }
Safe Diagnostics
Use field_keys() for logging without leaking secrets:
#![allow(unused)] fn main() { use quack_rs::secrets::SecretEntry; let entry = SecretEntry::new("key", "s3") .with_field("access_key", "AKIA...") .with_field("secret_key", "wJalr..."); // Safe for logging — returns keys only, no values let mut keys = entry.field_keys(); keys.sort_unstable(); // the fields are a HashMap, so the order is unspecified assert_eq!(keys, ["access_key", "secret_key"]); }
Debug Output
The Debug implementation redacts field values and the scope. For an entry
with a scope and one field, {:#?} prints:
SecretEntry {
name: "api_key",
secret_type: "bearer",
provider: "config",
scope: "[REDACTED]",
fields: {
"token": "[REDACTED]",
},
}
Security Best Practices
- Never log secret field values — use
field_keys()for diagnostics - Drop clones promptly — minimize the window during which sensitive data resides in memory
- Implement
remove_secretwith zeroization — don't just remove the reference; zeroize the data before deallocation - Thread safety —
SecretsManagerrequiresSend + Sync, because DuckDB may run the callbacks that consult it on several threads at once