669 lines
16 KiB
Markdown
669 lines
16 KiB
Markdown
---
|
|
description: Manage secrets and configuration for Rust Lambda functions using AWS Secrets Manager and Parameter Store
|
|
---
|
|
|
|
You are helping the user securely manage secrets and configuration for their Rust Lambda functions.
|
|
|
|
## Your Task
|
|
|
|
Guide the user through implementing secure secrets management using AWS Secrets Manager, Systems Manager Parameter Store, and the Parameters and Secrets Lambda Extension.
|
|
|
|
## Secrets Management Options
|
|
|
|
### Option 1: AWS Parameters and Secrets Lambda Extension (Recommended)
|
|
|
|
**Best for**:
|
|
- Production workloads
|
|
- Cost-conscious applications
|
|
- Low-latency requirements
|
|
- Local caching needs
|
|
|
|
**Advantages**:
|
|
- Cached locally (reduces latency and cost)
|
|
- No SDK calls needed
|
|
- Automatic refresh
|
|
- Works with both Secrets Manager and Parameter Store
|
|
|
|
#### Setup
|
|
|
|
1. **Add the extension layer** to your Lambda:
|
|
|
|
```bash
|
|
cargo lambda deploy \
|
|
--layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:11
|
|
```
|
|
|
|
For x86_64:
|
|
```bash
|
|
cargo lambda deploy \
|
|
--layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11
|
|
```
|
|
|
|
2. **Add IAM permissions**:
|
|
|
|
```json
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"secretsmanager:GetSecretValue",
|
|
"ssm:GetParameter"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-*",
|
|
"arn:aws:ssm:us-east-1:123456789012:parameter/myapp/*"
|
|
]
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": "kms:Decrypt",
|
|
"Resource": "arn:aws:kms:us-east-1:123456789012:key/key-id"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
3. **Use the Rust client**:
|
|
|
|
Add to `Cargo.toml`:
|
|
```toml
|
|
[dependencies]
|
|
aws-parameters-and-secrets-lambda = "0.1"
|
|
serde_json = "1"
|
|
```
|
|
|
|
Basic usage:
|
|
```rust
|
|
use aws_parameters_and_secrets_lambda::{Manager, ParameterError};
|
|
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
|
|
use std::env;
|
|
|
|
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
|
|
// Get secret from Secrets Manager
|
|
let manager = Manager::new();
|
|
let secret_value = manager
|
|
.get_secret("my-database-password")
|
|
.await?;
|
|
|
|
// Parse as JSON if needed
|
|
let db_config: DatabaseConfig = serde_json::from_str(&secret_value)?;
|
|
|
|
// Use the secret
|
|
let connection = connect_to_db(&db_config).await?;
|
|
|
|
Ok(Response { success: true })
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DatabaseConfig {
|
|
host: String,
|
|
port: u16,
|
|
username: String,
|
|
password: String,
|
|
database: String,
|
|
}
|
|
```
|
|
|
|
#### Get Parameter Store Values
|
|
|
|
```rust
|
|
use aws_parameters_and_secrets_lambda::Manager;
|
|
|
|
async fn get_config() -> Result<AppConfig, Error> {
|
|
let manager = Manager::new();
|
|
|
|
// Get simple parameter
|
|
let api_url = manager
|
|
.get_parameter("/myapp/api-url")
|
|
.await?;
|
|
|
|
// Get SecureString parameter (automatically decrypted)
|
|
let api_key = manager
|
|
.get_parameter("/myapp/api-key")
|
|
.await?;
|
|
|
|
Ok(AppConfig {
|
|
api_url,
|
|
api_key,
|
|
})
|
|
}
|
|
```
|
|
|
|
#### Caching and TTL
|
|
|
|
The extension caches secrets/parameters automatically. Configure TTL:
|
|
|
|
```bash
|
|
cargo lambda deploy \
|
|
--layers arn:aws:lambda:...:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:11 \
|
|
--env-var PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED=true \
|
|
--env-var PARAMETERS_SECRETS_EXTENSION_CACHE_SIZE=1000 \
|
|
--env-var PARAMETERS_SECRETS_EXTENSION_MAX_CONNECTIONS=3
|
|
```
|
|
|
|
### Option 2: AWS SDK Direct Calls
|
|
|
|
**Best for**:
|
|
- Simple use cases
|
|
- One-time secret retrieval
|
|
- When extension layer isn't available
|
|
|
|
#### Using AWS SDK for Secrets Manager
|
|
|
|
Add to `Cargo.toml`:
|
|
```toml
|
|
[dependencies]
|
|
aws-config = "1"
|
|
aws-sdk-secretsmanager = "1"
|
|
```
|
|
|
|
Usage:
|
|
```rust
|
|
use aws_config::BehaviorVersion;
|
|
use aws_sdk_secretsmanager::Client as SecretsManagerClient;
|
|
use std::sync::OnceLock;
|
|
|
|
static SECRETS_CLIENT: OnceLock<SecretsManagerClient> = OnceLock::new();
|
|
|
|
async fn get_secrets_client() -> &'static SecretsManagerClient {
|
|
SECRETS_CLIENT.get_or_init(|| async {
|
|
let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
|
|
SecretsManagerClient::new(&config)
|
|
}).await
|
|
}
|
|
|
|
async fn get_database_password() -> Result<String, Error> {
|
|
let client = get_secrets_client().await;
|
|
|
|
let response = client
|
|
.get_secret_value()
|
|
.secret_id("prod/database/password")
|
|
.send()
|
|
.await?;
|
|
|
|
Ok(response.secret_string().unwrap().to_string())
|
|
}
|
|
|
|
// For JSON secrets
|
|
async fn get_database_config() -> Result<DatabaseConfig, Error> {
|
|
let client = get_secrets_client().await;
|
|
|
|
let response = client
|
|
.get_secret_value()
|
|
.secret_id("prod/database/config")
|
|
.send()
|
|
.await?;
|
|
|
|
let secret_string = response.secret_string().unwrap();
|
|
let config: DatabaseConfig = serde_json::from_str(secret_string)?;
|
|
|
|
Ok(config)
|
|
}
|
|
```
|
|
|
|
#### Using AWS SDK for Parameter Store
|
|
|
|
Add to `Cargo.toml`:
|
|
```toml
|
|
[dependencies]
|
|
aws-config = "1"
|
|
aws-sdk-ssm = "1"
|
|
```
|
|
|
|
Usage:
|
|
```rust
|
|
use aws_sdk_ssm::Client as SsmClient;
|
|
use std::sync::OnceLock;
|
|
|
|
static SSM_CLIENT: OnceLock<SsmClient> = OnceLock::new();
|
|
|
|
async fn get_ssm_client() -> &'static SsmClient {
|
|
SSM_CLIENT.get_or_init(|| async {
|
|
let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
|
|
SsmClient::new(&config)
|
|
}).await
|
|
}
|
|
|
|
async fn get_parameter(name: &str) -> Result<String, Error> {
|
|
let client = get_ssm_client().await;
|
|
|
|
let response = client
|
|
.get_parameter()
|
|
.name(name)
|
|
.with_decryption(true) // Decrypt SecureString
|
|
.send()
|
|
.await?;
|
|
|
|
Ok(response.parameter().unwrap().value().unwrap().to_string())
|
|
}
|
|
|
|
// Get multiple parameters
|
|
async fn get_parameters_by_path(path: &str) -> Result<HashMap<String, String>, Error> {
|
|
let client = get_ssm_client().await;
|
|
|
|
let mut parameters = HashMap::new();
|
|
let mut next_token = None;
|
|
|
|
loop {
|
|
let mut request = client
|
|
.get_parameters_by_path()
|
|
.path(path)
|
|
.with_decryption(true)
|
|
.recursive(true);
|
|
|
|
if let Some(token) = next_token {
|
|
request = request.next_token(token);
|
|
}
|
|
|
|
let response = request.send().await?;
|
|
|
|
for param in response.parameters() {
|
|
parameters.insert(
|
|
param.name().unwrap().to_string(),
|
|
param.value().unwrap().to_string(),
|
|
);
|
|
}
|
|
|
|
next_token = response.next_token().map(|s| s.to_string());
|
|
if next_token.is_none() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(parameters)
|
|
}
|
|
```
|
|
|
|
### Option 3: Environment Variables (For Non-Sensitive Config)
|
|
|
|
**Best for**:
|
|
- Non-sensitive configuration
|
|
- Simple deployments
|
|
- Configuration that changes per environment
|
|
|
|
```rust
|
|
use std::env;
|
|
|
|
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
|
|
let api_url = env::var("API_URL")
|
|
.expect("API_URL must be set");
|
|
|
|
let timeout_secs: u64 = env::var("TIMEOUT_SECONDS")
|
|
.unwrap_or_else(|_| "30".to_string())
|
|
.parse()
|
|
.expect("TIMEOUT_SECONDS must be a number");
|
|
|
|
// Use configuration
|
|
let client = build_client(&api_url, timeout_secs);
|
|
|
|
Ok(Response { })
|
|
}
|
|
```
|
|
|
|
Deploy with environment variables:
|
|
```bash
|
|
cargo lambda deploy \
|
|
--env-var API_URL=https://api.example.com \
|
|
--env-var TIMEOUT_SECONDS=30 \
|
|
--env-var ENVIRONMENT=production
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Initialize Secrets at Startup
|
|
|
|
```rust
|
|
use std::sync::OnceLock;
|
|
|
|
struct AppSecrets {
|
|
database_password: String,
|
|
api_key: String,
|
|
encryption_key: String,
|
|
}
|
|
|
|
static SECRETS: OnceLock<AppSecrets> = OnceLock::new();
|
|
|
|
async fn init_secrets() -> Result<&'static AppSecrets, Error> {
|
|
SECRETS.get_or_try_init(|| async {
|
|
let manager = Manager::new();
|
|
|
|
Ok(AppSecrets {
|
|
database_password: manager.get_secret("db-password").await?,
|
|
api_key: manager.get_parameter("/myapp/api-key").await?,
|
|
encryption_key: manager.get_secret("encryption-key").await?,
|
|
})
|
|
}).await
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Error> {
|
|
// Load secrets once at startup
|
|
init_secrets().await?;
|
|
|
|
run(service_fn(function_handler)).await
|
|
}
|
|
|
|
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
|
|
// Access pre-loaded secrets
|
|
let secrets = SECRETS.get().unwrap();
|
|
|
|
let connection = connect_with_password(&secrets.database_password).await?;
|
|
|
|
Ok(Response {})
|
|
}
|
|
```
|
|
|
|
### 2. Separate Secrets by Environment
|
|
|
|
```
|
|
# Development
|
|
/dev/myapp/database/password
|
|
/dev/myapp/api-key
|
|
|
|
# Staging
|
|
/staging/myapp/database/password
|
|
/staging/myapp/api-key
|
|
|
|
# Production
|
|
/prod/myapp/database/password
|
|
/prod/myapp/api-key
|
|
```
|
|
|
|
Usage:
|
|
```rust
|
|
let env = std::env::var("ENVIRONMENT").unwrap_or_else(|_| "dev".to_string());
|
|
let param_name = format!("/{}/myapp/database/password", env);
|
|
|
|
let password = manager.get_parameter(¶m_name).await?;
|
|
```
|
|
|
|
### 3. Handle Secret Rotation
|
|
|
|
```rust
|
|
use std::sync::RwLock;
|
|
use std::time::{Duration, Instant};
|
|
|
|
struct CachedSecret {
|
|
value: String,
|
|
last_updated: Instant,
|
|
ttl: Duration,
|
|
}
|
|
|
|
static SECRET_CACHE: OnceLock<RwLock<HashMap<String, CachedSecret>>> = OnceLock::new();
|
|
|
|
async fn get_secret_with_ttl(name: &str, ttl: Duration) -> Result<String, Error> {
|
|
let cache = SECRET_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
|
|
|
|
// Check cache
|
|
{
|
|
let cache = cache.read().unwrap();
|
|
if let Some(cached) = cache.get(name) {
|
|
if cached.last_updated.elapsed() < cached.ttl {
|
|
return Ok(cached.value.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch new value
|
|
let manager = Manager::new();
|
|
let value = manager.get_secret(name).await?;
|
|
|
|
// Update cache
|
|
{
|
|
let mut cache = cache.write().unwrap();
|
|
cache.insert(name.to_string(), CachedSecret {
|
|
value: value.clone(),
|
|
last_updated: Instant::now(),
|
|
ttl,
|
|
});
|
|
}
|
|
|
|
Ok(value)
|
|
}
|
|
```
|
|
|
|
### 4. Validate Secrets Format
|
|
|
|
```rust
|
|
use thiserror::Error;
|
|
|
|
#[derive(Error, Debug)]
|
|
enum SecretError {
|
|
#[error("Invalid secret format: {0}")]
|
|
InvalidFormat(String),
|
|
|
|
#[error("Missing required field: {0}")]
|
|
MissingField(String),
|
|
}
|
|
|
|
fn validate_database_config(config: &DatabaseConfig) -> Result<(), SecretError> {
|
|
if config.host.is_empty() {
|
|
return Err(SecretError::MissingField("host".to_string()));
|
|
}
|
|
|
|
if config.port == 0 {
|
|
return Err(SecretError::InvalidFormat("Port must be non-zero".to_string()));
|
|
}
|
|
|
|
if config.password.len() < 12 {
|
|
return Err(SecretError::InvalidFormat(
|
|
"Password must be at least 12 characters".to_string()
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Creating Secrets
|
|
|
|
### Via AWS CLI
|
|
|
|
**Secrets Manager**:
|
|
```bash
|
|
# Simple string secret
|
|
aws secretsmanager create-secret \
|
|
--name prod/database/password \
|
|
--secret-string "MySuperSecretPassword123!"
|
|
|
|
# JSON secret
|
|
aws secretsmanager create-secret \
|
|
--name prod/database/config \
|
|
--secret-string '{
|
|
"host": "db.example.com",
|
|
"port": 5432,
|
|
"username": "app_user",
|
|
"password": "MySuperSecretPassword123!",
|
|
"database": "myapp"
|
|
}'
|
|
```
|
|
|
|
**Parameter Store**:
|
|
```bash
|
|
# String parameter
|
|
aws ssm put-parameter \
|
|
--name /myapp/api-url \
|
|
--value "https://api.example.com" \
|
|
--type String
|
|
|
|
# SecureString parameter (encrypted)
|
|
aws ssm put-parameter \
|
|
--name /myapp/api-key \
|
|
--value "sk_live_abc123" \
|
|
--type SecureString
|
|
|
|
# With KMS key
|
|
aws ssm put-parameter \
|
|
--name /myapp/encryption-key \
|
|
--value "my-encryption-key" \
|
|
--type SecureString \
|
|
--key-id alias/myapp-key
|
|
```
|
|
|
|
### Via Terraform
|
|
|
|
**Secrets Manager**:
|
|
```hcl
|
|
resource "aws_secretsmanager_secret" "database_password" {
|
|
name = "prod/database/password"
|
|
description = "Database password for production"
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret_version" "database_password" {
|
|
secret_id = aws_secretsmanager_secret.database_password.id
|
|
secret_string = var.database_password # From Terraform variables
|
|
}
|
|
|
|
# JSON secret
|
|
resource "aws_secretsmanager_secret" "database_config" {
|
|
name = "prod/database/config"
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret_version" "database_config" {
|
|
secret_id = aws_secretsmanager_secret.database_config.id
|
|
secret_string = jsonencode({
|
|
host = "db.example.com"
|
|
port = 5432
|
|
username = "app_user"
|
|
password = var.database_password
|
|
database = "myapp"
|
|
})
|
|
}
|
|
```
|
|
|
|
**Parameter Store**:
|
|
```hcl
|
|
resource "aws_ssm_parameter" "api_url" {
|
|
name = "/myapp/api-url"
|
|
type = "String"
|
|
value = "https://api.example.com"
|
|
}
|
|
|
|
resource "aws_ssm_parameter" "api_key" {
|
|
name = "/myapp/api-key"
|
|
type = "SecureString"
|
|
value = var.api_key
|
|
}
|
|
```
|
|
|
|
## Secrets Manager vs Parameter Store
|
|
|
|
| Feature | Secrets Manager | Parameter Store |
|
|
|---------|----------------|-----------------|
|
|
| Cost | $0.40/secret/month + API calls | Free (Standard), $0.05/param/month (Advanced) |
|
|
| Max size | 65 KB | 4 KB (Standard), 8 KB (Advanced) |
|
|
| Rotation | Built-in | Manual |
|
|
| Versioning | Yes | Yes |
|
|
| Cross-account | Yes | Yes (Advanced) |
|
|
| Best for | Passwords, API keys | Configuration, non-rotated secrets |
|
|
|
|
## Complete Example
|
|
|
|
```rust
|
|
use aws_parameters_and_secrets_lambda::Manager;
|
|
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
|
|
use serde::Deserialize;
|
|
use std::sync::OnceLock;
|
|
use tracing::info;
|
|
|
|
#[derive(Deserialize, Clone)]
|
|
struct AppConfig {
|
|
database: DatabaseConfig,
|
|
api_key: String,
|
|
feature_flags: FeatureFlags,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone)]
|
|
struct DatabaseConfig {
|
|
host: String,
|
|
port: u16,
|
|
username: String,
|
|
password: String,
|
|
database: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone)]
|
|
struct FeatureFlags {
|
|
new_feature_enabled: bool,
|
|
max_batch_size: usize,
|
|
}
|
|
|
|
static CONFIG: OnceLock<AppConfig> = OnceLock::new();
|
|
|
|
async fn load_config() -> Result<&'static AppConfig, Error> {
|
|
CONFIG.get_or_try_init(|| async {
|
|
let manager = Manager::new();
|
|
let env = std::env::var("ENVIRONMENT")?;
|
|
|
|
// Get database config from Secrets Manager
|
|
let db_secret = manager
|
|
.get_secret(&format!("{}/database/config", env))
|
|
.await?;
|
|
let database: DatabaseConfig = serde_json::from_str(&db_secret)?;
|
|
|
|
// Get API key from Parameter Store
|
|
let api_key = manager
|
|
.get_parameter(&format!("/{}/api-key", env))
|
|
.await?;
|
|
|
|
// Get feature flags from Parameter Store
|
|
let flags_json = manager
|
|
.get_parameter(&format!("/{}/feature-flags", env))
|
|
.await?;
|
|
let feature_flags: FeatureFlags = serde_json::from_str(&flags_json)?;
|
|
|
|
Ok(AppConfig {
|
|
database,
|
|
api_key,
|
|
feature_flags,
|
|
})
|
|
}).await
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Error> {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
// Load configuration at startup
|
|
info!("Loading configuration...");
|
|
load_config().await?;
|
|
info!("Configuration loaded successfully");
|
|
|
|
run(service_fn(function_handler)).await
|
|
}
|
|
|
|
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
|
|
let config = CONFIG.get().unwrap();
|
|
|
|
info!("Processing request with feature flags: {:?}", config.feature_flags);
|
|
|
|
// Use configuration
|
|
let db = connect_to_database(&config.database).await?;
|
|
let api_client = ApiClient::new(&config.api_key);
|
|
|
|
// Your business logic here
|
|
|
|
Ok(Response { success: true })
|
|
}
|
|
```
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] Use Secrets Manager for sensitive data (passwords, keys)
|
|
- [ ] Use Parameter Store for configuration
|
|
- [ ] Never log secret values
|
|
- [ ] Use IAM policies to restrict access
|
|
- [ ] Enable encryption at rest (KMS)
|
|
- [ ] Use separate secrets per environment
|
|
- [ ] Implement secret rotation
|
|
- [ ] Validate secret format at startup
|
|
- [ ] Cache secrets to reduce API calls
|
|
- [ ] Use extension layer for production
|
|
- [ ] Set appropriate TTL for cached secrets
|
|
- [ ] Monitor secret access in CloudTrail
|
|
- [ ] Use least privilege IAM permissions
|
|
|
|
Guide the user through implementing secure secrets management appropriate for their needs.
|