--- 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) -> Result { // 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 { 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 = 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 { 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 { 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 = 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 { 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, 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) -> Result { 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 = 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) -> Result { // 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>> = OnceLock::new(); async fn get_secret_with_ttl(name: &str, ttl: Duration) -> Result { 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 = 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) -> Result { 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.