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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
.with_expiration(datetime) | Set expiration time |
.with_ttl(duration) | Set TTL relative to now |
.with_metadata(key, value) | Add metadata |
Inspection Methods:
| Method | Description |
|---|---|
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:
| Variant | Fields | Use Case |
|---|---|---|
UsernamePassword | username, password | Database authentication |
Token | token | API key/token authentication |
AwsCredentials | access_key_id, secret_access_key, session_token? | AWS services |
Certificate | cert, key | mTLS client certificates |
OAuth2 | access_token, refresh_token?, token_type | OAuth2 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"
| Property | Required | Description |
|---|---|---|
url | Yes | Full connection URL with credentials |
username | Alternative | Username (with separate password) |
password | With username | Password |
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:
| Preset | Username | Password | Access Key | Secret Key | Session Token |
|---|---|---|---|---|---|
postgres_default() | PGUSER | PGPASSWORD | - | - | - |
mysql_default() | MYSQL_USER | MYSQL_PASSWORD | - | - | - |
mssql_default() | MSSQL_USER | MSSQL_PASSWORD | - | - | - |
aws_default() | - | - | AWS_ACCESS_KEY_ID | AWS_SECRET_ACCESS_KEY | AWS_SESSION_TOKEN |
with_prefix("X") | X_USERNAME | X_PASSWORD | X_ACCESS_KEY | X_SECRET_KEY | X_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:
| Format | Extension | Example |
|---|---|---|
| JSON | .json | {"username": "x", "password": "y"} |
| YAML | .yaml, .yml | username: xpassword: y |
| INI | .ini, .cfg | [default]username = x |
| ENV | .env | USERNAME=xPASSWORD=y |
| PEM | .pem, .crt, .key | Certificate/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"
| Property | Required | Description |
|---|---|---|
aws_secret_id | Yes | Secrets Manager secret ID or ARN |
region | No | AWS 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"
| Property | Required | Description |
|---|---|---|
use_iam_auth | Yes | Enable RDS IAM authentication |
host | Yes | RDS endpoint |
username | Yes | IAM 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:
| Method | Configuration |
|---|---|
| Token | auth_method = "token", token = "hvs.xxx" |
| AppRole | auth_method = "approle", role_id, secret_id |
| Kubernetes | auth_method = "kubernetes", role |
| AWS IAM | auth_method = "aws", role |
| TLS | auth_method = "tls", client certificate |
| UserPass | auth_method = "userpass", username, password |
Secret Types:
| Type | Description |
|---|---|
KvV2 | Key-Value v2 secrets engine (static secrets) |
Database | Database 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"
| Property | Required | Description |
|---|---|---|
addr | Yes | Vault server address |
auth_method | Yes | Authentication method |
vault_path | For KV | Path to KV secret |
vault_role | For DB | Database role name |
namespace | No | Vault 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"
| Property | Required | Description |
|---|---|---|
vault_pki_path | Yes | PKI issue path |
vault_pki_common_name | Yes | Certificate CN |
vault_pki_ttl | No | Certificate TTL |
vault_pki_dns_sans | No | DNS Subject Alt Names |
vault_pki_ip_sans | No | IP 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:
| Provider | Token Source |
|---|---|
| GitHub Actions | GITHUB_TOKEN or ACTIONS_ID_TOKEN_REQUEST_* |
| GitLab CI/CD | CI_JOB_JWT |
| Azure AD / Entra ID | Azure managed identity |
| Google Cloud | GCP service account |
| Kubernetes (EKS/GKE) | Service account token file |
| Custom | Token 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:
| Parameter | Default | Description |
|---|---|---|
refresh_buffer | 60s | How long before expiry to refresh |
max_cache_age | None | Max cache age for non-expiring credentials |
Cache Operations:
| Method | Description |
|---|---|
invalidate() | Force cache invalidation |
cache_stats() | Get cache statistics |
CacheStats Fields:
| Field | Type | Description |
|---|---|---|
is_cached | bool | Whether credentials are cached |
cache_age | Option<Duration> | How long cached |
time_to_refresh | Option<Duration> | Time until auto-refresh |
is_expired | bool | Whether 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
| Variant | Description | Retryable |
|---|---|---|
NotFound | Credentials not found | No |
Expired | Credentials expired | No |
ProviderUnavailable | Provider unreachable | Yes |
AuthFailed | Authentication failed | No |
Network | Network error | Yes |
Config | Configuration error | No |
FileRead | File read error | No |
FileParse | File parse error | No |
EnvVarNotSet | Missing env var | No |
EnvVarInvalid | Invalid env var value | No |
MissingField | Required field missing | No |
RefreshNotSupported | Refresh not supported | No |
RefreshFailed | Refresh failed | Maybe |
InvalidFormat | Invalid format | No |
Certificate | Certificate error | No |
PrivateKey | Private key error | No |
Error Helper Methods:
| Method | Description |
|---|---|
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
- Never log credentials - Use structured logging carefully
- Use environment variables - Avoid hardcoded credentials
- Enable caching - Reduce calls to external services
- Check expiration - Always verify before use
- Rotate regularly - Use providers that support rotation
- Least privilege - Grant minimal required permissions
Secure Configuration Order
- Vault/Secrets Manager (production)
- Environment variables (containers)
- Credential files with proper permissions (Kubernetes secrets)
- Never: hardcoded credentials in config files
Feature Flags
Credential providers are enabled via Cargo features:
| Feature | Providers Enabled |
|---|---|
vault | VaultCredentialProvider, VaultPkiProvider |
aws | SecretsManagerProvider, RdsIamProvider, SsoProvider, WebIdentityProvider |
Core providers (Static, Environment, File) are always available.
Database Backend Credentials
PostgreSQL
PostgreSQL credentials can be provided via:
| Method | Configuration |
|---|---|
| Environment | PGUSER, PGPASSWORD or custom prefix |
| Connection URL | postgresql://user:pass@host/database |
| Vault | KV secret or database secrets engine |
| AWS RDS IAM | Token-based authentication |
| AWS Secrets Manager | Stored 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:
| Method | Configuration |
|---|---|
| Environment | MYSQL_USER, MYSQL_PASSWORD or custom prefix |
| Connection URL | mysql://user:pass@host/database |
| Vault | KV secret or database secrets engine |
| AWS RDS IAM | Token-based authentication (Aurora MySQL) |
| AWS Secrets Manager | Stored 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:
| Method | Configuration |
|---|---|
| Environment | MSSQL_USER, MSSQL_PASSWORD or custom prefix |
| Connection URL | mssql://user:pass@host/database |
| Vault | KV secret or database secrets engine |
| Windows Authentication | Integrated security (Windows only) |
| Azure AD | Token-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.
| Method | Configuration |
|---|---|
| No credentials | Default for unencrypted databases |
| Environment | SQLITE_PASSPHRASE for encrypted databases |
| File | Passphrase 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
- Credential Providers How-To - Practical usage guide
- Vault Integration - HashiCorp Vault setup
- AWS Secrets - AWS credential management
- mTLS Authentication - Certificate authentication
- Configuration Reference - Full configuration options