# Reference Implementation Guide
This guide demonstrates a pattern for managing database credentials across multiple environments using TOML configuration files and 1Password secret references.
It's is by no means a production ready implementation to copy/paste for your projects. Use these examples as a guidance on how you _could_ implement this crate.
This reference implemetation uses:
1. **TOML files** to define which 1Password secrets to fetch per environment
2. **1Password** to securely stores the actual credential values
3. **`PgConnectOptions`** to connect, without exposing passwords as strings
## Directory Structure
```
your-project/
├── config/
│ ├── base.toml # Shared defaults
│ ├── development.toml # Dev 1Password references
│ ├── staging.toml # Staging 1Password references
│ └── production.toml # Production 1Password references
├── src/
│ ├── config.rs # Config loading logic
│ ├── secrets.rs # 1Password - corteq-onepassword crate integration
│ └── main.rs
└── Cargo.toml
```
## Example TOML Configurations
In these config examples we're using 1Password vaults per environment.
Decide if this is best for your environment or not.
You could use the same vault for all environments, and referecne differt items per environment. Make your own call here.
### `config/base.toml` - Shared Defaults
```toml
# Base configuration shared across all environments
# Environment-specific files override these values
# So no database 1Password references here
[server]
host = "0.0.0.0"
port = 8080
[database]
max_connections = 10
connect_timeout_secs = 30
[logging]
level = "info"
```
### `config/development.toml`
```toml
# Development environment configuration
# Uses local/development 1Password vault
[server]
port = 3000 # Override for local development
[database]
max_connections = 5
[database.secrets]
# 1Password references - secrets fetched at runtime
host = "op://Development/PostgreSQL-Local/host"
port = "op://Development/PostgreSQL-Local/port"
user = "op://Development/PostgreSQL-Local/username"
password = "op://Development/PostgreSQL-Local/password"
database = "op://Development/PostgreSQL-Local/database"
[logging]
level = "debug"
```
### `config/staging.toml`
```toml
# Staging environment configuration
# Uses staging 1Password vault
[database]
max_connections = 20
[database.secrets]
host = "op://Staging/PostgreSQL-Staging/host"
port = "op://Staging/PostgreSQL-Staging/port"
user = "op://Staging/PostgreSQL-Staging/username"
password = "op://Staging/PostgreSQL-Staging/password"
database = "op://Staging/PostgreSQL-Staging/database"
```
### `config/production.toml`
```toml
# Production environment configuration
# Uses production 1Password vault with restricted access
[database]
max_connections = 50
connect_timeout_secs = 10
[database.secrets]
host = "op://Production/PostgreSQL-Prod/host"
port = "op://Production/PostgreSQL-Prod/port"
user = "op://Production/PostgreSQL-Prod/username"
password = "op://Production/PostgreSQL-Prod/password"
database = "op://Production/PostgreSQL-Prod/database"
[logging]
level = "warn"
```
## Rust Implementation
This sections shows how you _could_ integrate the crate within your project.
### Dependencies (`Cargo.toml`)
First add the crate to your dependencies.
```toml
[dependencies]
corteq-onepassword = "0.1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
thiserror = "2"
```
### `src/config.rs` - Configuration Structures
This is not specific for this crate, but shows how you _could_ implement `.toml` config for your project.
We assume your project might already have something similar.
```rust
use std::path::Path;
use serde::Deserialize;
/// 1Password secret references for database credentials.
#[derive(Debug, Clone, Deserialize)]
pub struct SecretsConfig {
/// 1Password reference: "op://Vault/Item/host"
pub host: String,
/// 1Password reference: "op://Vault/Item/port"
pub port: String,
/// 1Password reference: "op://Vault/Item/username"
pub user: String,
/// 1Password reference: "op://Vault/Item/password"
pub password: String,
/// 1Password reference: "op://Vault/Item/database"
pub database: String,
}
/// Database configuration section.
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
/// 1Password secret references
pub secrets: SecretsConfig,
/// Maximum database connections
#[serde(default = "default_max_connections")]
pub max_connections: u32,
/// Connection timeout in seconds
#[serde(default = "default_timeout")]
pub connect_timeout_secs: u64,
}
fn default_max_connections() -> u32 { 10 }
fn default_timeout() -> u64 { 30 }
/// Server configuration section.
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
fn default_host() -> String { "0.0.0.0".to_string() }
fn default_port() -> u16 { 8080 }
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
}
}
}
/// Root configuration loaded from TOML.
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub database: DatabaseConfig,
#[serde(default)]
pub server: ServerConfig,
}
impl AppConfig {
/// Load configuration for the specified environment.
///
/// Merges base.toml with environment-specific config.
pub fn load(env: &str) -> Result<Self, ConfigError> {
let config_dir = Path::new("config");
// Load environment-specific config
// In production, you might merge base.toml + {env}.toml
let env_path = config_dir.join(format!("{}.toml", env));
let content = std::fs::read_to_string(&env_path)
.map_err(|e| ConfigError::FileRead {
path: env_path.display().to_string(),
source: e,
})?;
let config: AppConfig = toml::from_str(&content)
.map_err(|e| ConfigError::Parse {
path: env_path.display().to_string(),
source: e,
})?;
Ok(config)
}
/// Load configuration based on APP_ENV environment variable.
///
/// Defaults to "development" if not set.
pub fn from_env() -> Result<Self, ConfigError> {
let env = std::env::var("APP_ENV")
.unwrap_or_else(|_| "development".to_string());
Self::load(&env)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file '{path}': {source}")]
FileRead {
path: String,
#[source]
source: std::io::Error,
},
#[error("Failed to parse config file '{path}': {source}")]
Parse {
path: String,
#[source]
source: toml::de::Error,
},
}
```
### `src/secrets.rs` - 1Password Integration
And now on to the actual implementation of the crate.
In this example we're using a `DatabaseSecrets` struct for PostgreSQL connection details (host, port, user, password, database), with the password stored as a `SecretString` to automatically zeroes memory when secret is dropped. Preventing secrets from lingering in memory where they could be exposed through dumps or debugging.
```rust
use corteq_onepassword::{ExposeSecret, OnePassword, SecretString};
use sqlx::postgres::PgConnectOptions;
use crate::config::SecretsConfig;
/// Database credentials fetched from 1Password.
///
/// Password is stored as `SecretString` for automatic memory zeroization.
/// `SecretString` automatically shows "[REDACTED]" in Debug output.
#[derive(Debug)]
pub struct DatabaseSecrets {
pub host: String,
pub port: u16,
pub user: String,
pub password: SecretString, // Auto-redacted in Debug output
pub database: String,
}
// Implement Display for logging (password masked)
impl std::fmt::Display for DatabaseSecrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"postgresql://{}:***@{}:{}/{}",
self.user, self.host, self.port, self.database
)
}
}
impl DatabaseSecrets {
/// Create `PgConnectOptions` without exposing password in a URL string.
///
/// This is the preferred method - the password is only exposed at the
/// moment of connection, not persisted in memory as a String.
pub fn to_connect_options(&self) -> PgConnectOptions {
PgConnectOptions::new()
.host(&self.host)
.port(self.port)
.username(&self.user)
.password(self.password.expose_secret())
.database(&self.database)
}
}
pub async fn fetch_database_secrets(
config: &SecretsConfig,
) -> Result<DatabaseSecrets, SecretsError> {
// Initialize 1Password client from OP_SERVICE_ACCOUNT_TOKEN
let client = OnePassword::from_env()
.map_err(|e| SecretsError::ClientInit(e.to_string()))?
// Meta data for audit logs on 1Password side
.integration("my-app", env!("CARGO_PKG_VERSION"))
.connect()
.await
.map_err(|e| SecretsError::Connection(e.to_string()))?;
// Fetch all secrets in a single batch request
let secrets = client
.secrets_named(&[
("host", config.host.as_str()),
("port", config.port.as_str()),
("user", config.user.as_str()),
("password", config.password.as_str()),
("database", config.database.as_str()),
])
.await
.map_err(|e| SecretsError::Fetch(e.to_string()))?;
// Extract each secret
let host = secrets
.get("host")
.ok_or(SecretsError::Missing("host"))?
.expose_secret()
.to_string();
let port_str = secrets
.get("port")
.ok_or(SecretsError::Missing("port"))?
.expose_secret();
let port: u16 = port_str
.parse()
.map_err(|_| SecretsError::InvalidPort(port_str.to_string()))?;
let user = secrets
.get("user")
.ok_or(SecretsError::Missing("user"))?
.expose_secret()
.to_string();
let password = secrets
.get("password")
.ok_or(SecretsError::Missing("password"))?
.clone();
let database = secrets
.get("database")
.ok_or(SecretsError::Missing("database"))?
.expose_secret()
.to_string();
Ok(DatabaseSecrets {
host,
port,
user,
password,
database,
})
}
#[derive(Debug, thiserror::Error)]
pub enum SecretsError {
#[error("Failed to initialize 1Password client: {0}")]
ClientInit(String),
#[error("Failed to connect to 1Password: {0}")]
Connection(String),
#[error("Failed to fetch secrets: {0}")]
Fetch(String),
#[error("Missing secret: {0}")]
Missing(&'static str),
#[error("Invalid port value: {0}")]
InvalidPort(String),
}
```
### `src/main.rs` - Application Bootstrap
Now we bring everything together for the main app.
```rust
mod config;
mod secrets;
use config::AppConfig;
use secrets::fetch_database_secrets;
use sqlx::postgres::PgPoolOptions;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env file for OP_SERVICE_ACCOUNT_TOKEN
dotenvy::dotenv().ok();
// Determine environment from APP_ENV (defaults to "development")
let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string());
println!("Starting application in {} mode", env);
// Load TOML configuration for current environment
let config = AppConfig::load(&env)?;
// Fetch database credentials from 1Password
println!("Fetching database credentials from 1Password...");
let db_secrets = fetch_database_secrets(&config.database.secrets).await?;
// Log connection info (password is automatically masked)
println!("Connecting to database: {}", db_secrets);
// Create connection pool using PgConnectOptions
// Password is never stored as a plain String
let pool = PgPoolOptions::new()
.max_connections(config.database.max_connections)
.connect_with(db_secrets.to_connect_options())
.await?;
println!("Connected to database successfully!");
// Verify connection
let row: (i64,) = sqlx::query_as("SELECT 1")
.fetch_one(&pool)
.await?;
println!("Database query test: {}", row.0);
// Start your application server here...
println!("Server listening on {}:{}", config.server.host, config.server.port);
Ok(())
}
```
## Usage
### 1. Set Up 1Password
Create items in your 1Password vaults:
```
Vault: Development
└── Item: PostgreSQL-Local
├── host: localhost
├── port: 5432
├── username: dev_user
├── password: dev_password
└── database: myapp_dev
Vault: Production
└── Item: PostgreSQL-Prod
├── host: prod-db.example.com
├── port: 5432
├── username: prod_user
├── password: (secure password)
└── database: myapp_prod
```
### 2. Configure Service Account
Create a 1Password service account with access to the appropriate vaults:
```bash
# Development - access to Development vault only
export OP_SERVICE_ACCOUNT_DEV_TOKEN="ops_..."
# Production - access to Production vault only
export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
```
### 3. Running the Application
```bash
# Development (default)
cargo run
# Staging
APP_ENV=staging cargo run
# Production
APP_ENV=production cargo run
```
## Best Practices
### 1. Never Store Passwords as Strings
```rust
// BAD: Password persists in memory
let url = format!("postgresql://{}:{}@{}:{}/{}",
user, password, host, port, database);
// GOOD: Password only exposed at connection time
let options = PgConnectOptions::new()
.password(secret.expose_secret());
```
### 2. Use Separate Vaults per Environment
- Development vault: All developers have access
- Staging vault: CI/CD and QA team access
- Production vault: Restricted to operations team
### 3. Use Different Service Accounts
Each environment should have its own service account token with minimal permissions:
```bash
# .env.development
OP_SERVICE_ACCOUNT_DEV_TOKEN=ops_xxx
# .env.production (managed by deployment system)
OP_SERVICE_ACCOUNT_TOKEN=ops_xxx
```
### 4. Automatic Secret Redaction
`SecretString` automatically redacts values in `Debug` output:
```rust
pub struct DatabaseSecrets {
pub password: SecretString, // Shows "[REDACTED]" when debug-printed
}
// This is safe - password won't be exposed
println!("{:?}", db_secrets);
// Output: DatabaseSecrets { password: Secret([REDACTED]), ... }
```
No custom `Debug` implementation needed when using `SecretString`.
### 5. Batch Secret Fetches
Fetch all secrets in one request to minimize API calls:
```rust
// GOOD: Single batch request
let secrets = client.secrets_named(&[
("host", "op://..."),
("port", "op://..."),
("password", "op://..."),
]).await?;
// BAD: Multiple individual requests
let host = client.secret("op://...").await?;
let port = client.secret("op://...").await?;
let password = client.secret("op://...").await?;
```
## Troubleshooting
### "OP_SERVICE_ACCOUNT_TOKEN not set"
Ensure the environment variable is set:
```bash
export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
# or use .env file with dotenvy
```
### "Access denied to vault"
Verify the service account has access to the vault specified in your 1Password references.
### "Secret not found"
Check that:
1. The vault name matches exactly (case-sensitive)
2. The item name matches exactly
3. The field name matches exactly
```toml
# Correct format
password = "op://VaultName/ItemName/fieldname"
```
## Security Considerations
1. **Token Storage**: Never commit `OP_SERVICE_ACCOUNT_TOKEN` to version control
2. **Vault Isolation**: Use separate vaults per environment to limit blast radius
3. **Audit Logging**: 1Password logs all secret access for compliance
4. **Token Rotation**: Rotate your service account tokens periodically
5. **Memory Safety**: Use `SecretString` to automatically zeroizes memory on drop