Files
gh-emillindfors-claude-mark…/commands/lambda-secrets.md
2025-11-29 18:25:52 +08:00

16 KiB

description
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

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:
cargo lambda deploy \
  --layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:11

For x86_64:

cargo lambda deploy \
  --layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11
  1. Add IAM permissions:
{
  "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"
    }
  ]
}
  1. Use the Rust client:

Add to Cargo.toml:

[dependencies]
aws-parameters-and-secrets-lambda = "0.1"
serde_json = "1"

Basic usage:

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

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:

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:

[dependencies]
aws-config = "1"
aws-sdk-secretsmanager = "1"

Usage:

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:

[dependencies]
aws-config = "1"
aws-sdk-ssm = "1"

Usage:

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
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:

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

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:

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(&param_name).await?;

3. Handle Secret Rotation

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

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:

# 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:

# 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:

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:

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

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.