Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Credential Provider Reference

Complete reference for the Horizon Epoch credential provider abstraction. This document covers the architecture, all available providers, credential types, caching, and error handling.

Architecture Overview

The credential provider system provides a unified abstraction for managing credentials across all storage backends.

                    ┌─────────────────────────────────────────┐
                    │          Storage Backend                │
                    │  (PostgreSQL, S3, Vault, etc.)          │
                    └─────────────────────┬───────────────────┘
                                          │
                                          │ get_credentials()
                                          ▼
                    ┌─────────────────────────────────────────┐
                    │       CachedCredentialProvider          │
                    │  (optional caching layer)               │
                    └─────────────────────┬───────────────────┘
                                          │
          ┌───────────────────────────────┼───────────────────────────────┐
          │                               │                               │
          ▼                               ▼                               ▼
┌─────────────────┐             ┌─────────────────┐             ┌─────────────────┐
│    Static       │             │  Environment    │             │     File        │
│   Provider      │             │   Provider      │             │   Provider      │
└─────────────────┘             └─────────────────┘             └─────────────────┘
          │                               │                               │
          ▼                               ▼                               ▼
┌─────────────────┐             ┌─────────────────┐             ┌─────────────────┐
│     Vault       │             │     AWS         │             │   Web Identity  │
│   Provider      │             │   Provider      │             │   Provider      │
└─────────────────┘             └─────────────────┘             └─────────────────┘

Core Trait: CredentialProvider

All credential providers implement this async trait:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait CredentialProvider: Send + Sync {
    /// Retrieves credentials from this provider.
    async fn get_credentials(&self) -> CredentialResult<StorageCredentials>;

    /// Returns true if credentials need refresh.
    fn requires_refresh(&self) -> bool;

    /// Refreshes the credentials.
    async fn refresh(&self) -> CredentialResult<StorageCredentials>;

    /// Returns the recommended refresh interval.
    fn refresh_interval(&self) -> Option<Duration>;

    /// Returns the provider name for logging.
    fn provider_name(&self) -> &str;

    /// Returns true if refresh is supported.
    fn supports_refresh(&self) -> bool {
        self.refresh_interval().is_some()
    }

    /// Returns metadata for diagnostics.
    fn provider_info(&self) -> ProviderInfo;
}
}

Extension Trait: CredentialProviderExt

Additional utility methods available on all providers:

MethodDescription
get_fresh_credentials()Gets credentials, refreshing first if needed
validate()Validates that credentials can be retrieved

Credential Types

StorageCredentials

The unified credential container with expiration tracking:

#![allow(unused)]
fn main() {
pub struct StorageCredentials {
    /// The specific type of credential
    pub credential_type: StorageCredentialType,

    /// When these credentials expire (optional)
    pub expires_at: Option<DateTime<Utc>>,

    /// Additional metadata
    pub metadata: HashMap<String, String>,
}
}

Factory Methods:

MethodDescription
username_password(user, pass)Create username/password credentials
token(token)Create token-based credentials
aws(access_key, secret_key, session_token)Create AWS credentials
oauth2(access_token, refresh_token, token_type)Create OAuth2 credentials
certificate(cert, key)Create certificate credentials

Builder Methods:

MethodDescription
.with_expiration(datetime)Set expiration time
.with_ttl(duration)Set TTL relative to now
.with_metadata(key, value)Add metadata

Inspection Methods:

MethodDescription
is_expired()Check if credentials are expired
time_to_expiry()Get time until expiration
is_username_password()Type check
is_token()Type check
is_aws()Type check
is_certificate()Type check
is_oauth2()Type check

StorageCredentialType

The underlying credential type enum:

VariantFieldsUse Case
UsernamePasswordusername, passwordDatabase authentication
TokentokenAPI key/token authentication
AwsCredentialsaccess_key_id, secret_access_key, session_token?AWS services
Certificatecert, keymTLS client certificates
OAuth2access_token, refresh_token?, token_typeOAuth2 authentication

Certificate Types

CertificateData:

#![allow(unused)]
fn main() {
pub struct CertificateData {
    pub pem: SecretString,           // PEM-encoded certificate
    pub chain: Option<Vec<SecretString>>,  // Optional chain
}
}

PrivateKeyData:

#![allow(unused)]
fn main() {
pub struct PrivateKeyData {
    pub pem: SecretString,           // PEM-encoded private key
    pub passphrase: Option<SecretString>,  // Optional passphrase
}
}

Built-in Providers

Static Provider

Pre-configured credentials for testing and development. Never use in production.

Factory Methods:

#![allow(unused)]
fn main() {
StaticCredentialProvider::username_password("user", "pass")
StaticCredentialProvider::token("api-key")
StaticCredentialProvider::aws("AKIA...", "secret", None)
StaticCredentialProvider::oauth2("access", Some("refresh"), "Bearer")
StaticCredentialProvider::certificate(cert_data, key_data)
}

Builder Pattern:

#![allow(unused)]
fn main() {
StaticCredentialProviderBuilder::new()
    .username("admin")
    .password("secret")
    .build()
}

Configuration (TOML):

[storage.postgres.mydb]
# Static credentials embedded in config (dev only!)
url = "postgresql://user:password@localhost/db"
PropertyRequiredDescription
urlYesFull connection URL with credentials
usernameAlternativeUsername (with separate password)
passwordWith usernamePassword

Refresh Support: No


Environment Provider

Reads credentials from environment variables.

Factory Methods:

#![allow(unused)]
fn main() {
// PostgreSQL standard: PGUSER, PGPASSWORD
EnvCredentialProvider::postgres_default()

// AWS standard: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
EnvCredentialProvider::aws_default()

// Custom prefix: MYAPP_DB_USERNAME, MYAPP_DB_PASSWORD
EnvCredentialProvider::with_prefix("MYAPP_DB")
}

Builder Pattern:

#![allow(unused)]
fn main() {
EnvCredentialProviderBuilder::new()
    .username_var("DB_USER")
    .password_var("DB_PASS")
    .credential_type(EnvCredentialType::UsernamePassword)
    .build()
}

Environment Variable Mappings:

PresetUsernamePasswordAccess KeySecret KeySession Token
postgres_default()PGUSERPGPASSWORD---
mysql_default()MYSQL_USERMYSQL_PASSWORD---
mssql_default()MSSQL_USERMSSQL_PASSWORD---
aws_default()--AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN
with_prefix("X")X_USERNAMEX_PASSWORDX_ACCESS_KEYX_SECRET_KEYX_SESSION_TOKEN

Configuration (TOML):

[storage.postgres.mydb]
url = "${EPOCH_POSTGRES_MYDB_URL}"  # Variable substitution

# Or individual fields
host = "db.example.com"
username = "${DB_USER}"
password = "${DB_PASS}"

Refresh Support: Yes (re-reads environment on refresh)


File Provider

Reads credentials from files in multiple formats.

Factory Methods:

#![allow(unused)]
fn main() {
FileCredentialProvider::json("/etc/secrets/db.json")
FileCredentialProvider::yaml("/etc/secrets/db.yaml")
FileCredentialProvider::ini("/etc/secrets/db.ini")
FileCredentialProvider::env("/etc/secrets/.env")
FileCredentialProvider::aws_credentials()  // ~/.aws/credentials
FileCredentialProvider::certificate("/path/cert.pem", "/path/key.pem")
}

Builder Pattern:

#![allow(unused)]
fn main() {
FileCredentialProviderBuilder::new()
    .path("/etc/secrets/creds.json")
    .format(CredentialFileFormat::Json)
    .profile("production")  // For INI files
    .build()
}

Supported Formats:

FormatExtensionExample
JSON.json{"username": "x", "password": "y"}
YAML.yaml, .ymlusername: x
password: y
INI.ini, .cfg[default]
username = x
ENV.envUSERNAME=x
PASSWORD=y
PEM.pem, .crt, .keyCertificate/key files

Configuration (TOML):

[storage.postgres.mydb]
password_file = "/run/secrets/db-password"

# Or JSON credentials file
credentials_file = "/etc/secrets/db.json"

Refresh Support: Yes (re-reads file on refresh)


AWS Provider

Retrieves credentials from AWS services.

Secrets Manager Provider

#![allow(unused)]
fn main() {
SecretsManagerProvider::new("my-secret-id")
    .region("us-east-1")
    .field_mapping(SecretFieldMapping::default())
    .build()
}

Configuration (TOML):

[storage.postgres.mydb]
aws_secret_id = "prod/db/credentials"

[aws]
region = "us-east-1"
PropertyRequiredDescription
aws_secret_idYesSecrets Manager secret ID or ARN
regionNoAWS region (default: from environment)

RDS IAM Provider

Generates temporary IAM authentication tokens for RDS.

#![allow(unused)]
fn main() {
RdsIamProvider::new()
    .host("mydb.cluster-xxx.us-east-1.rds.amazonaws.com")
    .port(5432)
    .username("iam_user")
    .region("us-east-1")
    .build()
}

Configuration (TOML):

[storage.postgres.mydb]
host = "mydb.cluster-xxx.us-east-1.rds.amazonaws.com"
use_iam_auth = true
username = "iam_user"
PropertyRequiredDescription
use_iam_authYesEnable RDS IAM authentication
hostYesRDS endpoint
usernameYesIAM database user

Credential Sources

#![allow(unused)]
fn main() {
pub enum AwsCredentialSource {
    /// Default AWS credential chain
    DefaultChain,

    /// Explicit credentials
    Explicit {
        access_key_id: String,
        secret_access_key: String,
        session_token: Option<String>,
    },

    /// Assume a role
    AssumeRole {
        role_arn: String,
        external_id: Option<String>,
        session_name: Option<String>,
        session_tags: Option<HashMap<String, String>>,
    },

    /// Named AWS profile
    Profile { name: String },

    /// Web Identity (OIDC)
    WebIdentity { /* ... */ },
}
}

Refresh Support: Yes (automatic token refresh)


Vault Provider

Retrieves credentials from HashiCorp Vault.

#![allow(unused)]
fn main() {
VaultCredentialProvider::new(VaultConfig {
    addr: "https://vault.example.com:8200".to_string(),
    namespace: Some("my-namespace".to_string()),
    auth: VaultAuthMethod::AppRole {
        role_id: "xxx".to_string(),
        secret_id: "yyy".to_string(),
    },
    tls: Some(VaultTlsConfig {
        ca_cert: Some("/etc/ssl/vault-ca.crt".to_string()),
        client_cert: None,
        client_key: None,
    }),
})
.secret_path("secret/data/mydb")
.secret_type(VaultSecretType::KvV2)
.build()
}

Authentication Methods:

MethodConfiguration
Tokenauth_method = "token", token = "hvs.xxx"
AppRoleauth_method = "approle", role_id, secret_id
Kubernetesauth_method = "kubernetes", role
AWS IAMauth_method = "aws", role
TLSauth_method = "tls", client certificate
UserPassauth_method = "userpass", username, password

Secret Types:

TypeDescription
KvV2Key-Value v2 secrets engine (static secrets)
DatabaseDatabase secrets engine (dynamic credentials)

Configuration (TOML):

[vault]
addr = "https://vault.example.com:8200"
auth_method = "approle"
role_id = "xxx-xxx-xxx"
secret_id_file = "/run/secrets/vault-secret-id"
ca_cert = "/etc/ssl/certs/vault-ca.crt"
namespace = "my-namespace"

[storage.postgres.mydb]
vault_path = "secret/data/mydb"
# Or for dynamic credentials:
vault_role = "my-db-role"
PropertyRequiredDescription
addrYesVault server address
auth_methodYesAuthentication method
vault_pathFor KVPath to KV secret
vault_roleFor DBDatabase role name
namespaceNoVault namespace

Refresh Support: Yes (lease renewal, token refresh)


Vault PKI Provider

Issues X.509 certificates from Vault’s PKI secrets engine.

#![allow(unused)]
fn main() {
VaultPkiProvider::new(vault_config)
    .pki_path("pki/issue/my-role")
    .common_name("myapp.example.com")
    .ttl(Duration::from_secs(86400))
    .dns_sans(vec!["myapp.example.com", "myapp-alt.example.com"])
    .build()
}

Configuration (TOML):

[storage.postgres.secure]
vault_pki_path = "pki/issue/postgres-client"
vault_pki_common_name = "epoch-client"
vault_pki_ttl = "24h"
PropertyRequiredDescription
vault_pki_pathYesPKI issue path
vault_pki_common_nameYesCertificate CN
vault_pki_ttlNoCertificate TTL
vault_pki_dns_sansNoDNS Subject Alt Names
vault_pki_ip_sansNoIP Subject Alt Names

Refresh Support: Yes (automatic certificate renewal)


Web Identity Provider

Uses OIDC tokens for AWS authentication.

#![allow(unused)]
fn main() {
WebIdentityCredentialProvider::new(WebIdentityConfig {
    role_arn: "arn:aws:iam::123456789012:role/MyRole".to_string(),
    web_identity_token_file: Some("/var/run/secrets/token".to_string()),
    session_name: Some("my-session".to_string()),
    provider_url: Some("https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E".to_string()),
    duration_seconds: Some(3600),
    policy_arns: None,
    inline_policy: None,
})
.build()
}

Supported OIDC Providers:

ProviderToken Source
GitHub ActionsGITHUB_TOKEN or ACTIONS_ID_TOKEN_REQUEST_*
GitLab CI/CDCI_JOB_JWT
Azure AD / Entra IDAzure managed identity
Google CloudGCP service account
Kubernetes (EKS/GKE)Service account token file
CustomToken file or direct token

Configuration (TOML):

[aws]
role_arn = "arn:aws:iam::123456789012:role/MyRole"
web_identity_token_file = "/var/run/secrets/token"

Refresh Support: Yes (token refresh on expiration)


SSO Provider

Integrates with AWS IAM Identity Center (formerly AWS SSO).

#![allow(unused)]
fn main() {
SsoCredentialProvider::new(SsoConfig {
    start_url: "https://my-sso-portal.awsapps.com/start".to_string(),
    region: "us-east-1".to_string(),
    account_id: "123456789012".to_string(),
    role_name: "MyRole".to_string(),
})
.build()
}

Uses the OAuth 2.0 Device Authorization Grant flow for browser-based authentication.

Configuration (TOML):

[aws.sso]
start_url = "https://my-sso-portal.awsapps.com/start"
region = "us-east-1"
account_id = "123456789012"
role_name = "MyRole"

Refresh Support: Yes (automatic token refresh, cached tokens)


Credential Caching

Wrap any provider with CachedCredentialProvider to reduce external calls.

#![allow(unused)]
fn main() {
// Simple caching
let cached = CachedCredentialProvider::new(inner_provider);

// With configuration
let cached = CachedCredentialProvider::new(inner_provider)
    .with_refresh_buffer(Duration::from_secs(120))   // Refresh 2 min before expiry
    .with_max_cache_age(Duration::from_secs(3600));  // Max 1 hour cache

// Using extension trait
let cached = inner_provider.with_cache();

// Using builder
let cached = inner_provider.cached()
    .refresh_buffer(Duration::from_secs(60))
    .max_cache_age(Duration::from_secs(3600))
    .build();
}

Cache Configuration:

ParameterDefaultDescription
refresh_buffer60sHow long before expiry to refresh
max_cache_ageNoneMax cache age for non-expiring credentials

Cache Operations:

MethodDescription
invalidate()Force cache invalidation
cache_stats()Get cache statistics

CacheStats Fields:

FieldTypeDescription
is_cachedboolWhether credentials are cached
cache_ageOption<Duration>How long cached
time_to_refreshOption<Duration>Time until auto-refresh
is_expiredboolWhether cached credentials expired

Configuration (TOML):

[credentials]
cache_enabled = true
cache_ttl = 300           # 5 minutes
cache_max_size = 100      # Max entries
refresh_before_expiry = 60  # Refresh 60s before expiry

Provider Chains

Chain multiple providers for fallback behavior:

#![allow(unused)]
fn main() {
use std::sync::Arc;

let env_provider = Arc::new(EnvCredentialProvider::postgres_default());
let file_provider = Arc::new(FileCredentialProvider::json("/etc/db.json"));
let vault_provider = Arc::new(VaultCredentialProvider::new(config));

let chain = ChainedCredentialProvider::new(vec![
    env_provider,
    file_provider,
    vault_provider,
]).with_name("my-credential-chain");

// Tries each provider in order until one succeeds
let creds = chain.get_credentials().await?;
}

Chain Behavior:

  • Tries providers in order
  • Returns first successful result
  • Falls through on any error
  • Collects last error if all fail
  • Refresh interval = minimum of all providers

Error Types

CredentialError Variants

VariantDescriptionRetryable
NotFoundCredentials not foundNo
ExpiredCredentials expiredNo
ProviderUnavailableProvider unreachableYes
AuthFailedAuthentication failedNo
NetworkNetwork errorYes
ConfigConfiguration errorNo
FileReadFile read errorNo
FileParseFile parse errorNo
EnvVarNotSetMissing env varNo
EnvVarInvalidInvalid env var valueNo
MissingFieldRequired field missingNo
RefreshNotSupportedRefresh not supportedNo
RefreshFailedRefresh failedMaybe
InvalidFormatInvalid formatNo
CertificateCertificate errorNo
PrivateKeyPrivate key errorNo

Error Helper Methods:

MethodDescription
is_not_found()Check if NotFound
is_expired()Check if Expired
is_retryable()Check if safe to retry

Implementing Custom Providers

Implement the CredentialProvider trait for custom credential sources:

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use horizon_epoch_core::storage::credentials::{
    CredentialProvider, StorageCredentials, CredentialError, CredentialResult,
};
use std::time::Duration;

struct MyProvider {
    config: MyConfig,
}

#[async_trait]
impl CredentialProvider for MyProvider {
    async fn get_credentials(&self) -> CredentialResult<StorageCredentials> {
        // Fetch from your credential source
        let secret = self.fetch_secret().await?;

        Ok(StorageCredentials::username_password(
            secret.username,
            secret.password,
        ).with_ttl(Duration::from_secs(3600)))
    }

    fn requires_refresh(&self) -> bool {
        // Check if credentials need refresh
        false
    }

    async fn refresh(&self) -> CredentialResult<StorageCredentials> {
        // Refresh credentials
        self.get_credentials().await
    }

    fn refresh_interval(&self) -> Option<Duration> {
        // Recommended refresh interval
        Some(Duration::from_secs(300))
    }

    fn provider_name(&self) -> &str {
        "my_provider"
    }
}
}

Security Considerations

Secret Protection

All sensitive values use secrecy::SecretString:

  • Not printed in Debug output
  • Zeroized on drop
  • Explicit .expose_secret() required

Best Practices

  1. Never log credentials - Use structured logging carefully
  2. Use environment variables - Avoid hardcoded credentials
  3. Enable caching - Reduce calls to external services
  4. Check expiration - Always verify before use
  5. Rotate regularly - Use providers that support rotation
  6. Least privilege - Grant minimal required permissions

Secure Configuration Order

  1. Vault/Secrets Manager (production)
  2. Environment variables (containers)
  3. Credential files with proper permissions (Kubernetes secrets)
  4. Never: hardcoded credentials in config files

Feature Flags

Credential providers are enabled via Cargo features:

FeatureProviders Enabled
vaultVaultCredentialProvider, VaultPkiProvider
awsSecretsManagerProvider, RdsIamProvider, SsoProvider, WebIdentityProvider

Core providers (Static, Environment, File) are always available.


Database Backend Credentials

PostgreSQL

PostgreSQL credentials can be provided via:

MethodConfiguration
EnvironmentPGUSER, PGPASSWORD or custom prefix
Connection URLpostgresql://user:pass@host/database
VaultKV secret or database secrets engine
AWS RDS IAMToken-based authentication
AWS Secrets ManagerStored credentials

Example (Environment):

export PGUSER="myuser"
export PGPASSWORD="mypassword"

Example (Vault):

[storage.postgres.prod]
vault_path = "secret/data/postgres/prod"

MySQL

MySQL credentials can be provided via:

MethodConfiguration
EnvironmentMYSQL_USER, MYSQL_PASSWORD or custom prefix
Connection URLmysql://user:pass@host/database
VaultKV secret or database secrets engine
AWS RDS IAMToken-based authentication (Aurora MySQL)
AWS Secrets ManagerStored credentials

Example (Environment):

export MYSQL_USER="myuser"
export MYSQL_PASSWORD="mypassword"

Example (TOML):

[storage.mysql.analytics]
host = "mysql.example.com"
database = "analytics"
username = "${MYSQL_USER}"
password = "${MYSQL_PASSWORD}"

SQL Server (MSSQL)

SQL Server credentials can be provided via:

MethodConfiguration
EnvironmentMSSQL_USER, MSSQL_PASSWORD or custom prefix
Connection URLmssql://user:pass@host/database
VaultKV secret or database secrets engine
Windows AuthenticationIntegrated security (Windows only)
Azure ADToken-based authentication (Azure SQL)

Example (Environment):

export MSSQL_USER="sa"
export MSSQL_PASSWORD="YourPassword123!"

Example (TOML):

[storage.mssql.warehouse]
host = "sqlserver.example.com"
database = "warehouse"
username = "${MSSQL_USER}"
password = "${MSSQL_PASSWORD}"
encrypt = true
trust_cert = false

Example (Windows Authentication):

[storage.mssql.warehouse]
host = "sqlserver.internal"
database = "warehouse"
use_windows_auth = true

SQLite

SQLite typically does not require credentials as it uses file-based storage. However, encrypted SQLite databases (SQLCipher) may require a passphrase.

MethodConfiguration
No credentialsDefault for unencrypted databases
EnvironmentSQLITE_PASSPHRASE for encrypted databases
FilePassphrase file for SQLCipher

Example (Unencrypted):

[storage.sqlite.local]
path = "/data/local.db"
# No credentials needed

Example (Encrypted with SQLCipher):

[storage.sqlite.secure]
path = "/data/secure.db"
passphrase = "${SQLITE_PASSPHRASE}"

See Also