Skip to content

Add a credential to your tool's config

Embedding a CredentialRef gives operators all four configuration routes — environment variable, OS keychain, literal, ecosystem fallback — without you writing any of them.

Add the dependency

[dependencies]
rtb-credentials = "0.6"
tokio = { version = "1", features = ["full"] }

tokio is needed because the store methods are async and KeyringStore requires a Tokio runtime specifically.

Embed the reference in your config struct

use rtb_credentials::CredentialRef;

#[derive(serde::Deserialize)]
struct Config {
    anthropic: CredentialRef,
}

Your config struct can derive Deserialize but not Serialize once it holds a CredentialRef. If you need to write config back, keep credential fields in a separate struct from the one you serialise.

Resolve it where the secret is used

Resolve at the point of use rather than at startup. Nothing is cached, but resolving late keeps the secret out of memory until it is needed and means a credential that is never used never has to be present.

use rtb_credentials::{ExposeSecret, Resolver};

let resolver = Resolver::with_platform_default();
let key = resolver.resolve(&config.anthropic).await?;

let header = format!("Bearer {}", key.expose_secret());

with_platform_default() builds a resolver over the OS keychain. To supply a different backend for the keychain leg, use Resolver::new(Arc::new(my_store)).

What operators can now write

All four keys are optional, and any combination is valid:

# an environment variable this tool defines
anthropic:
  env: MYTOOL_ANTHROPIC_KEY

# the OS keychain
anthropic:
  keychain:
    service: mytool
    account: anthropic

# an ecosystem-wide variable, as a last resort
anthropic:
  fallback_env: ANTHROPIC_API_KEY

Sources are tried in the order env, keychain, literal, fallback_env, and the first hit wins.

Give the credential a fallback_env if a conventional variable exists

Setting fallback_env to an ecosystem name such as ANTHROPIC_API_KEY means the tool works on a machine already configured for that provider, with no config at all.

// in whatever supplies your defaults
CredentialRef {
    fallback_env: Some("ANTHROPIC_API_KEY".to_string()),
    ..CredentialRef::default()
}

Because it is the last leg, it never overrides something the operator configured deliberately for your tool.

Handle the failure cases you have just created

use rtb_credentials::CredentialError;

match resolver.resolve(&config.anthropic).await {
    Ok(secret) => { /* use it */ }
    Err(CredentialError::NotFound { name }) => {
        // nothing configured — name is a hint, not the full list of
        // sources tried
    }
    Err(CredentialError::LiteralRefusedInCi) => {
        // a literal is configured and CI=true; the fallback_env leg
        // was NOT tried
    }
    Err(CredentialError::Keychain(msg)) => {
        // the keychain backend failed; resolution stopped here and
        // lower-precedence sources were not tried
    }
    Err(e) => return Err(e.into()),
}

CredentialError is #[non_exhaustive], so a wildcard arm is required.

Do not check whether the secret is non-empty by resolving it

A variable that is set but empty resolves successfully to an empty SecretString, and stops the chain. If an empty credential is a problem for your tool, check for it explicitly after resolving:

if key.expose_secret().is_empty() {
    // the source was configured but held nothing
}

Nothing in the crate does this for you.