A serverless AWS Lambda function demonstrating secure cross-account role assumption patterns for SaaS applications. This example shows how to implement external tool authentication in a serverless environment using AWS Lambda, API Gateway, and proper IAM configurations.
- Serverless Architecture: Built with AWS Lambda and API Gateway
- Cross-Account Access: Secure role assumption with external ID validation
- RESTful API: Clean API endpoints for various operations
- Comprehensive Logging: Structured logging with CloudWatch integration
- Error Handling: Proper error responses and dead letter queue configuration
- Infrastructure as Code: Complete CloudFormation/SAM template
- Security Best Practices: Least privilege IAM policies and secure credential handling
- Monitoring & Alerting: CloudWatch alarms and X-Ray tracing
- Production Ready: Includes throttling, CORS, and environment configurations
- AWS CLI configured with appropriate permissions
- AWS SAM CLI installed
- Go 1.21 or later
- Docker (for local testing)
# Navigate to the lambda function directory
cd examples/lambda-function
# Build the function
sam build
# Deploy with guided configuration (first time)
sam deploy --guided
# Or deploy with parameters
sam deploy \
--parameter-overrides \
Environment=dev \
CrossAccountRoleArn=arn:aws:iam::123456789012:role/MyRole \
ExternalId=my-external-id# Get the API Gateway URL from outputs
API_URL=$(aws cloudformation describe-stacks \
--stack-name lambda-function \
--query 'Stacks[0].Outputs[?OutputKey==`ApiGatewayUrl`].OutputValue' \
--output text)
# Health check
curl -X POST "$API_URL/" \
-H "Content-Type: application/json" \
-d '{"action": "health_check"}'
# Get caller identity
curl -X POST "$API_URL/" \
-H "Content-Type: application/json" \
-d '{"action": "get_caller_identity"}'
# List S3 buckets
curl -X POST "$API_URL/" \
-H "Content-Type: application/json" \
-d '{"action": "list_s3_buckets"}'# Assume cross-account role
curl -X POST "$API_URL/" \
-H "Content-Type: application/json" \
-d '{
"action": "assume_role",
"target_role": "arn:aws:iam::123456789012:role/CrossAccountRole",
"external_id": "your-external-id"
}'
# List S3 buckets with assumed role
curl -X POST "$API_URL/" \
-H "Content-Type: application/json" \
-d '{
"action": "list_s3_buckets",
"target_role": "arn:aws:iam::123456789012:role/CrossAccountRole",
"external_id": "your-external-id"
}'All requests are sent to the API Gateway endpoint as POST requests with JSON payloads.
{
"action": "health_check"
}Response:
{
"success": true,
"message": "Lambda function is healthy",
"data": {
"status": "healthy",
"timestamp": "2024-01-15T10:30:00Z"
}
}{
"action": "get_caller_identity",
"target_role": "arn:aws:iam::123456789012:role/Role" // optional
}Response:
{
"success": true,
"message": "Caller identity retrieved successfully",
"data": {
"user_id": "AIDACKCEVSQ6C2EXAMPLE",
"account": "123456789012",
"arn": "arn:aws:sts::123456789012:assumed-role/lambda-role/lambda-function"
}
}{
"action": "assume_role",
"target_role": "arn:aws:iam::123456789012:role/CrossAccountRole",
"external_id": "your-external-id"
}Response:
{
"success": true,
"message": "Role assumed successfully",
"data": {
"user_id": "AIDACKCEVSQ6C2EXAMPLE",
"account": "123456789012",
"arn": "arn:aws:sts::123456789012:assumed-role/CrossAccountRole/lambda-function-session",
"expires_at": "2024-01-15T11:30:00Z",
"session_name": "lambda-function-session"
}
}{
"action": "list_s3_buckets",
"target_role": "arn:aws:iam::123456789012:role/Role", // optional
"external_id": "your-external-id" // optional
}Response:
{
"success": true,
"message": "Found 3 S3 buckets",
"data": {
"buckets": [
{
"name": "my-bucket-1",
"creation_date": "2024-01-01T00:00:00Z"
},
{
"name": "my-bucket-2",
"creation_date": "2024-01-02T00:00:00Z"
}
],
"count": 2
}
}All errors follow this format:
{
"success": false,
"error": "Error description here"
}Common HTTP status codes:
200- Success400- Bad Request (invalid action, missing parameters)401- Unauthorized (authentication failed)500- Internal Server Error (AWS API errors, Lambda errors)
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ API Gateway │───▶│ Lambda │───▶│ Cross-Account │
│ │ │ Function │ │ Role │
└─────────────────┘ └──────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ CloudWatch │ │ SQS │ │ Target AWS │
│ Logs │ │ DLQ │ │ Services │
└─────────────────┘ └──────────────┘ └─────────────────┘
- Lambda Execution Role: Minimal permissions for basic Lambda operation
- Cross-Account Role Assumption: Uses STS AssumeRole with external ID
- Temporary Credentials: All AWS operations use temporary, time-limited credentials
- External ID Validation: Prevents confused deputy attacks
- Least Privilege: Each role has only the permissions needed for its function
- Client sends API request to API Gateway
- API Gateway invokes Lambda function
- Lambda function validates request and assumes target role (if specified)
- Lambda function performs AWS operations with assumed credentials
- Response is returned through API Gateway to client
- All operations are logged to CloudWatch
The Lambda function uses these environment variables:
ENVIRONMENT=dev # Deployment environment
LOG_LEVEL=INFO # Logging level
CROSS_ACCOUNT_ROLE_ARN=... # Default cross-account role ARN
EXTERNAL_ID=... # External ID for role assumption
FUNCTION_NAME=... # Lambda function name
API_GATEWAY_URL=... # API Gateway endpoint URLCustomize deployment with these parameters:
# template.yaml parameters
Environment: dev # Environment (dev/staging/prod)
LogLevel: INFO # Log level
CrossAccountRoleArn: "" # Cross-account role ARN
ExternalId: "" # External ID for cross-account accessThe stack provides these outputs:
ApiGatewayUrl- API Gateway endpoint URLLambdaFunctionName- Lambda function nameLambdaFunctionArn- Lambda function ARNLambdaExecutionRoleArn- Lambda execution role ARN
# Install dependencies
go mod download
# Run tests
go test ./...
# Build for deployment
GOOS=linux GOARCH=amd64 go build -o main main.go
# Local testing with SAM
sam local start-api
# Invoke function directly
sam local invoke RemoteAccessFunction -e events/test-event.jsonexamples/lambda-function/
├── main.go # Lambda function entry point
├── template.yaml # SAM template
├── go.mod # Go module definition
├── README.md # This file
├── events/ # Test events
│ ├── health-check.json
│ ├── assume-role.json
│ └── list-s3.json
└── tests/ # Test files
└── main_test.go
Create test events in the events/ directory:
// events/health-check.json
{
"httpMethod": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json"
},
"body": "{\"action\": \"health_check\"}",
"requestContext": {
"identity": {
"sourceIp": "127.0.0.1"
}
}
}- Add a new case to the switch statement in
HandleRequest - Implement the handler function
- Add appropriate IAM permissions to the execution role
- Update API documentation
- Create test events
Example:
case "my_new_action":
responseBody, statusCode = f.handleMyNewAction(ctx, requestBody)
func (f *LambdaFunction) handleMyNewAction(ctx context.Context, req RequestBody) (ResponseBody, int) {
// Implementation here
return ResponseBody{
Success: true,
Message: "Action completed successfully",
Data: result,
}, 200
}sam deploy --parameter-overrides Environment=dev LogLevel=DEBUGsam deploy \
--parameter-overrides \
Environment=staging \
CrossAccountRoleArn=arn:aws:iam::123456789012:role/StagingRole \
ExternalId=staging-external-idsam deploy \
--parameter-overrides \
Environment=prod \
LogLevel=WARN \
CrossAccountRoleArn=arn:aws:iam::123456789012:role/ProductionRole \
ExternalId=production-external-id# .github/workflows/deploy.yml
name: Deploy Lambda Function
on:
push:
branches: [main]
paths: ['examples/lambda-function/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- uses: aws-actions/setup-sam@v2
- name: SAM build
run: |
cd examples/lambda-function
sam build
- name: SAM deploy
run: |
cd examples/lambda-function
sam deploy --no-confirm-changeset --no-fail-on-empty-changesetThe function automatically generates these metrics:
AWS/Lambda/Duration- Function execution timeAWS/Lambda/Errors- Function errorsAWS/Lambda/Invocations- Function invocationsAWS/Lambda/Throttles- Function throttles
Pre-configured alarms:
- Error Alarm: Triggers when errors exceed 5 in 10 minutes
- Duration Alarm: Triggers when average duration exceeds 25 seconds
Enable X-Ray tracing for detailed performance analysis:
aws lambda update-function-configuration \
--function-name lambda-function-RemoteAccessFunction \
--tracing-config Mode=ActiveAdd custom metrics to your function:
import "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
// Emit custom metric
func (f *LambdaFunction) emitMetric(metricName string, value float64) {
// CloudWatch metrics implementation
}- Least Privilege: Grant only necessary permissions
- Resource-Based Policies: Use specific resource ARNs when possible
- Condition Keys: Use condition keys to restrict access
- External ID: Always use external ID for cross-account access
- Regular Audits: Review and rotate access keys regularly
# Generate secure external ID
EXTERNAL_ID=$(openssl rand -hex 32)
# Store securely (e.g., AWS Systems Manager Parameter Store)
aws ssm put-parameter \
--name "/lambda-function/external-id" \
--value "$EXTERNAL_ID" \
--type "SecureString" \
--description "External ID for cross-account role assumption"For enhanced security, deploy in a VPC:
# Add to template.yaml
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2Error: Failed to assume role: Access Denied
Solutions:
- Verify the target role exists and is correctly configured
- Check that the external ID matches exactly
- Ensure the Lambda execution role has
sts:AssumeRolepermissions - Verify the trust policy on the target role allows assumption from Lambda
Error: Task timed out after 30.00 seconds
Solutions:
- Increase Lambda timeout in template.yaml
- Optimize function code for better performance
- Check for network connectivity issues
- Review CloudWatch logs for bottlenecks
Error: Access Denied when listing S3 buckets
Solutions:
- Verify S3 permissions on the assumed role
- Check bucket policies and ACLs
- Ensure the assumed role has the required S3 permissions
Enable debug logging:
sam deploy --parameter-overrides LogLevel=DEBUG# View Lambda logs
aws logs tail /aws/lambda/dev-remote-access-function --follow
# View API Gateway logs
aws logs tail /aws/apigateway/dev-remote-access-api --follow- Simple SaaS Example - Basic cross-account access
- CLI Tool Example - Interactive command-line tool
- Desktop App Example - GUI application
- AWS Lambda Developer Guide
- AWS SAM Developer Guide
- Cross-Account Role Assumption
- Security Best Practices
- AWS Lambda - Serverless compute
- API Gateway - REST API endpoints
- CloudFormation/SAM - Infrastructure as Code
- IAM - Identity and Access Management
- STS - Security Token Service
- CloudWatch - Logging and monitoring
- X-Ray - Distributed tracing
- SQS - Dead letter queue
This example is part of the AWS Remote Access Patterns project and follows the same MIT license.
This Lambda function example demonstrates how to implement secure cross-account access patterns in a serverless environment, providing a foundation for building scalable SaaS applications with proper security controls.