AWS SAM Security in Java: Complete Secure Serverless Application Implementation

AWS SAM (Serverless Application Model) security involves securing serverless applications through IAM roles, resource policies, environment variables, and application-level security measures. This guide provides a comprehensive Java implementation for building secure SAM applications.


Overview and Security Concepts

AWS SAM Security Components:

  • IAM Roles & Policies: Least privilege access control
  • Resource Policies: API Gateway, S3, DynamoDB policies
  • Environment Security: Secure environment variables
  • Network Security: VPC, security groups, subnets
  • Application Security: Input validation, encryption, logging

Key Security Areas:

  • Authentication & Authorization: Cognito, API Keys, IAM Auth
  • Data Protection: Encryption at rest and in transit
  • Auditing & Monitoring: CloudTrail, CloudWatch, X-Ray
  • Compliance: GDPR, HIPAA, SOC 2 controls

Dependencies and Setup

Maven Dependencies
<properties>
<aws.java.sdk.version>2.20.0</aws.java.sdk.version>
<spring.boot.version>3.1.0</spring.boot.version>
<aws.lambda.java.version>1.2.2</aws.lambda.java.version>
</properties>
<dependencies>
<!-- AWS SDK v2 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-core</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>lambda</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<!-- AWS Lambda Java Core -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>${aws.lambda.java.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.11.0</version>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.31</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
SAM Template (template.yml)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: java17
Environment:
Variables:
LOG_LEVEL: INFO
POWERTOOLS_SERVICE_NAME: secure-sam-app
PARAMETER_STORE_PREFIX: /app/secure-sam
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Resources:
# Security Roles
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SecureAppPermissions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: !GetRef UsersTable
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Sub ${SecureBucket.Arn}/*
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- kms:Decrypt
Resource: "*"
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${PARAMETER_STORE_PREFIX}/*
# API Gateway with Security
SecureApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Auth:
DefaultAuthorizer: AWS_IAM
AddDefaultAuthorizerToCorsPreflight: false
UsagePlan:
CreateUsagePlan: SHARED
Throttle:
BurstLimit: 100
RateLimit: 200
# Lambda Functions
UserManagementFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: target/secure-sam-app-1.0.0.jar
Handler: com.example.secureapp.UserManagementHandler::handleRequest
Role: !GetAtt LambdaExecutionRole.Arn
Events:
CreateUser:
Type: Api
Properties:
Path: /users
Method: post
RestApiId: !Ref SecureApi
Auth:
Authorizer: AWS_IAM
GetUser:
Type: Api
Properties:
Path: /users/{userId}
Method: get
RestApiId: !Ref SecureApi
FileProcessingFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: target/secure-sam-app-1.0.0.jar
Handler: com.example.secureapp.FileProcessingHandler::handleRequest
Role: !GetAtt LambdaExecutionRole.Arn
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Events:
ProcessFile:
Type: Api
Properties:
Path: /files
Method: post
RestApiId: !Ref SecureApi
Auth:
Authorizer: AWS_IAM
# DynamoDB Tables with Encryption
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: email
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: EmailIndex
KeySchema:
- AttributeName: email
KeyType: HASH
Projection:
ProjectionType: ALL
SSESpecification:
SSEEnabled: true
# S3 Bucket with Encryption
SecureBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
# Security Group for Lambda in VPC
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Lambda functions
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Outputs:
ApiUrl:
Description: "Secure API Gateway URL"
Value: !Sub "https://${SecureApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}"
UserManagementFunction:
Description: "User Management Lambda Function ARN"
Value: !GetAtt UserManagementFunction.Arn
SecureBucketName:
Description: "Secure S3 Bucket Name"
Value: !Ref SecureBucket

Core Security Components

1. Security Configuration
@Configuration
@EnableConfigurationProperties(SamSecurityProperties.class)
public class SecurityConfig {
@Bean
public AwsCredentialsProvider awsCredentialsProvider() {
return DefaultCredentialsProvider.create();
}
@Bean
public DynamoDbClient dynamoDbClient(AwsCredentialsProvider credentialsProvider) {
return DynamoDbClient.builder()
.credentialsProvider(credentialsProvider)
.region(Region.US_EAST_1)
.build();
}
@Bean
public S3Client s3Client(AwsCredentialsProvider credentialsProvider) {
return S3Client.builder()
.credentialsProvider(credentialsProvider)
.region(Region.US_EAST_1)
.build();
}
@Bean
public SecretsManagerClient secretsManagerClient(AwsCredentialsProvider credentialsProvider) {
return SecretsManagerClient.builder()
.credentialsProvider(credentialsProvider)
.region(Region.US_EAST_1)
.build();
}
@Bean
public KmsClient kmsClient(AwsCredentialsProvider credentialsProvider) {
return KmsClient.builder()
.credentialsProvider(credentialsProvider)
.region(Region.US_EAST_1)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}
@ConfigurationProperties(prefix = "app.security")
@Data
public class SamSecurityProperties {
private String kmsKeyId;
private String secretsPrefix = "/app/secure-sam";
private int bcryptStrength = 12;
private int jwtExpirationMs = 86400000; // 24 hours
private String corsAllowedOrigins = "*";
private EncryptionConfig encryption = new EncryptionConfig();
@Data
public static class EncryptionConfig {
private String algorithm = "AES/GCM/NoPadding";
private int keySize = 256;
}
}
2. IAM Policy Manager
@Service
@Slf4j
public class IamPolicyManager {
private final ObjectMapper objectMapper;
public IamPolicyManager(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String generateLambdaExecutionPolicy(String tableArn, String bucketArn, 
String kmsKeyArn, String secretsPrefix) {
try {
Map<String, Object> policy = new HashMap<>();
policy.put("Version", "2012-10-17");
List<Map<String, Object>> statements = new ArrayList<>();
// DynamoDB permissions
statements.add(createStatement(
"Allow",
List.of("dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", 
"dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan"),
List.of(tableArn, tableArn + "/index/*")
));
// S3 permissions
statements.add(createStatement(
"Allow",
List.of("s3:GetObject", "s3:PutObject", "s3:DeleteObject"),
List.of(bucketArn + "/*")
));
// KMS permissions
statements.add(createStatement(
"Allow",
List.of("kms:Decrypt", "kms:GenerateDataKey"),
List.of(kmsKeyArn)
));
// Secrets Manager permissions
statements.add(createStatement(
"Allow",
List.of("secretsmanager:GetSecretValue"),
List.of("arn:aws:secretsmanager:*:*:secret:" + secretsPrefix + "*")
));
policy.put("Statement", statements);
return objectMapper.writeValueAsString(policy);
} catch (Exception e) {
log.error("Failed to generate IAM policy", e);
throw new RuntimeException("IAM policy generation failed", e);
}
}
public boolean validatePolicy(String policyJson) {
try {
Map<String, Object> policy = objectMapper.readValue(policyJson, Map.class);
// Validate policy structure
if (!policy.containsKey("Version") || !policy.containsKey("Statement")) {
return false;
}
List<Map<String, Object>> statements = (List<Map<String, Object>>) policy.get("Statement");
for (Map<String, Object> statement : statements) {
if (!statement.containsKey("Effect") || !statement.containsKey("Action") || 
!statement.containsKey("Resource")) {
return false;
}
}
return true;
} catch (Exception e) {
log.error("Policy validation failed", e);
return false;
}
}
private Map<String, Object> createStatement(String effect, List<String> actions, List<String> resources) {
Map<String, Object> statement = new HashMap<>();
statement.put("Effect", effect);
statement.put("Action", actions);
statement.put("Resource", resources);
return statement;
}
public String createResourcePolicy(String sourceArn, String awsAccountId, String region) {
try {
Map<String, Object> policy = new HashMap<>();
policy.put("Version", "2012-10-17");
List<Map<String, Object>> statements = new ArrayList<>();
Map<String, Object> statement = new HashMap<>();
statement.put("Effect", "Allow");
statement.put("Principal", Map.of("AWS", "*"));
statement.put("Action", "execute-api:Invoke");
statement.put("Resource", "execute-api:/*");
// Add condition for source ARN
Map<String, Object> condition = new HashMap<>();
condition.put("ArnLike", Map.of("AWS:SourceArn", sourceArn));
statement.put("Condition", condition);
statements.add(statement);
policy.put("Statement", statements);
return objectMapper.writeValueAsString(policy);
} catch (Exception e) {
log.error("Failed to create resource policy", e);
throw new RuntimeException("Resource policy creation failed", e);
}
}
}
3. Secrets Management Service
@Service
@Slf4j
public class SecretsManagementService {
private final SecretsManagerClient secretsManager;
private final KmsClient kmsClient;
private final SamSecurityProperties securityProperties;
public SecretsManagementService(SecretsManagerClient secretsManager,
KmsClient kmsClient,
SamSecurityProperties securityProperties) {
this.secretsManager = secretsManager;
this.kmsClient = kmsClient;
this.securityProperties = securityProperties;
}
public String getSecret(String secretName) {
try {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(securityProperties.getSecretsPrefix() + "/" + secretName)
.build();
GetSecretValueResponse response = secretsManager.getSecretValue(request);
return response.secretString();
} catch (Exception e) {
log.error("Failed to retrieve secret: {}", secretName, e);
throw new RuntimeException("Secret retrieval failed: " + secretName, e);
}
}
public void storeSecret(String secretName, String secretValue) {
try {
// Encrypt the secret before storing
String encryptedSecret = encryptSecret(secretValue);
CreateSecretRequest request = CreateSecretRequest.builder()
.name(securityProperties.getSecretsPrefix() + "/" + secretName)
.description("Application secret for " + secretName)
.secretString(encryptedSecret)
.build();
secretsManager.createSecret(request);
log.info("Secret stored successfully: {}", secretName);
} catch (Exception e) {
log.error("Failed to store secret: {}", secretName, e);
throw new RuntimeException("Secret storage failed: " + secretName, e);
}
}
public void rotateSecret(String secretName, String newSecretValue) {
try {
String encryptedSecret = encryptSecret(newSecretValue);
UpdateSecretRequest request = UpdateSecretRequest.builder()
.secretId(securityProperties.getSecretsPrefix() + "/" + secretName)
.secretString(encryptedSecret)
.build();
secretsManager.updateSecret(request);
log.info("Secret rotated successfully: {}", secretName);
} catch (Exception e) {
log.error("Failed to rotate secret: {}", secretName, e);
throw new RuntimeException("Secret rotation failed: " + secretName, e);
}
}
public void deleteSecret(String secretName) {
try {
DeleteSecretRequest request = DeleteSecretRequest.builder()
.secretId(securityProperties.getSecretsPrefix() + "/" + secretName)
.forceDeleteWithoutRecovery(true)
.build();
secretsManager.deleteSecret(request);
log.info("Secret deleted successfully: {}", secretName);
} catch (Exception e) {
log.error("Failed to delete secret: {}", secretName, e);
throw new RuntimeException("Secret deletion failed: " + secretName, e);
}
}
private String encryptSecret(String secret) {
try {
EncryptRequest request = EncryptRequest.builder()
.keyId(securityProperties.getKmsKeyId())
.plaintext(SdkBytes.fromUtf8String(secret))
.build();
EncryptResponse response = kmsClient.encrypt(request);
return Base64.getEncoder().encodeToString(response.ciphertextBlob().asByteArray());
} catch (Exception e) {
log.error("Failed to encrypt secret", e);
throw new RuntimeException("Secret encryption failed", e);
}
}
private String decryptSecret(String encryptedSecret) {
try {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedSecret);
DecryptRequest request = DecryptRequest.builder()
.keyId(securityProperties.getKmsKeyId())
.ciphertextBlob(SdkBytes.fromByteArray(encryptedBytes))
.build();
DecryptResponse response = kmsClient.decrypt(request);
return response.plaintext().asUtf8String();
} catch (Exception e) {
log.error("Failed to decrypt secret", e);
throw new RuntimeException("Secret decryption failed", e);
}
}
public Map<String, String> getAllSecrets() {
try {
ListSecretsRequest request = ListSecretsRequest.builder()
.filters(Filter.builder()
.key("name")
.values(securityProperties.getSecretsPrefix())
.build())
.build();
ListSecretsResponse response = secretsManager.listSecrets(request);
return response.secretList().stream()
.collect(Collectors.toMap(
secret -> secret.name().replace(securityProperties.getSecretsPrefix() + "/", ""),
secret -> getSecret(secret.name().replace(securityProperties.getSecretsPrefix() + "/", ""))
));
} catch (Exception e) {
log.error("Failed to list secrets", e);
return Collections.emptyMap();
}
}
}
4. Encryption Service
@Service
@Slf4j
public class EncryptionService {
private final KmsClient kmsClient;
private final SamSecurityProperties securityProperties;
public EncryptionService(KmsClient kmsClient, SamSecurityProperties securityProperties) {
this.kmsClient = kmsClient;
this.securityProperties = securityProperties;
}
public String encryptData(String plaintext) {
try {
EncryptRequest request = EncryptRequest.builder()
.keyId(securityProperties.getKmsKeyId())
.plaintext(SdkBytes.fromUtf8String(plaintext))
.build();
EncryptResponse response = kmsClient.encrypt(request);
return Base64.getEncoder().encodeToString(response.ciphertextBlob().asByteArray());
} catch (Exception e) {
log.error("Failed to encrypt data", e);
throw new RuntimeException("Data encryption failed", e);
}
}
public String decryptData(String encryptedData) {
try {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
DecryptRequest request = DecryptRequest.builder()
.keyId(securityProperties.getKmsKeyId())
.ciphertextBlob(SdkBytes.fromByteArray(encryptedBytes))
.build();
DecryptResponse response = kmsClient.decrypt(request);
return response.plaintext().asUtf8String();
} catch (Exception e) {
log.error("Failed to decrypt data", e);
throw new RuntimeException("Data decryption failed", e);
}
}
public String generateDataKey() {
try {
GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
.keyId(securityProperties.getKmsKeyId())
.keySpec(DataKeySpec.AES_256)
.build();
GenerateDataKeyResponse response = kmsClient.generateDataKey(request);
Map<String, String> dataKey = new HashMap<>();
dataKey.put("ciphertext", Base64.getEncoder().encodeToString(response.ciphertextBlob().asByteArray()));
dataKey.put("plaintext", Base64.getEncoder().encodeToString(response.plaintext().asByteArray()));
return new ObjectMapper().writeValueAsString(dataKey);
} catch (Exception e) {
log.error("Failed to generate data key", e);
throw new RuntimeException("Data key generation failed", e);
}
}
public boolean validateEncryption(String encryptedData) {
try {
// Attempt to decrypt to validate the encryption
decryptData(encryptedData);
return true;
} catch (Exception e) {
return false;
}
}
public Map<String, String> getEncryptionContext() {
return Map.of(
"service", "secure-sam-app",
"environment", System.getenv("ENVIRONMENT") != null ? System.getenv("ENVIRONMENT") : "dev",
"timestamp", Instant.now().toString()
);
}
}

Lambda Handlers with Security

1. User Management Handler
@Component
@Slf4j
public class UserManagementHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final UserService userService;
private final SecurityValidator securityValidator;
private final ObjectMapper objectMapper;
public UserManagementHandler(UserService userService, 
SecurityValidator securityValidator,
ObjectMapper objectMapper) {
this.userService = userService;
this.securityValidator = securityValidator;
this.objectMapper = objectMapper;
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
try {
log.info("Processing request: {} {}", input.getHttpMethod(), input.getPath());
// Validate IAM authorization
if (!securityValidator.validateIamAuthorization(input)) {
return createErrorResponse(403, "Access denied");
}
String httpMethod = input.getHttpMethod();
String path = input.getPath();
switch (httpMethod) {
case "POST":
if ("/users".equals(path)) {
return createUser(input);
}
break;
case "GET":
if (path.startsWith("/users/")) {
return getUser(input);
}
break;
case "PUT":
if (path.startsWith("/users/")) {
return updateUser(input);
}
break;
case "DELETE":
if (path.startsWith("/users/")) {
return deleteUser(input);
}
break;
}
return createErrorResponse(404, "Endpoint not found");
} catch (Exception e) {
log.error("Request processing failed", e);
return createErrorResponse(500, "Internal server error");
}
}
private APIGatewayProxyResponseEvent createUser(APIGatewayProxyRequestEvent input) {
try {
// Validate input
if (input.getBody() == null) {
return createErrorResponse(400, "Request body is required");
}
CreateUserRequest request = objectMapper.readValue(input.getBody(), CreateUserRequest.class);
// Validate request
Set<ConstraintViolation<CreateUserRequest>> violations = securityValidator.validateRequest(request);
if (!violations.isEmpty()) {
return createValidationErrorResponse(violations);
}
// Create user
User user = userService.createUser(request);
return createSuccessResponse(201, user);
} catch (Exception e) {
log.error("User creation failed", e);
return createErrorResponse(400, "Invalid request format");
}
}
private APIGatewayProxyResponseEvent getUser(APIGatewayProxyRequestEvent input) {
try {
String userId = extractUserId(input.getPath());
if (userId == null) {
return createErrorResponse(400, "User ID is required");
}
// Authorize access - user can only access their own data unless admin
if (!securityValidator.canAccessUser(input, userId)) {
return createErrorResponse(403, "Access denied to user data");
}
User user = userService.getUser(userId);
if (user == null) {
return createErrorResponse(404, "User not found");
}
return createSuccessResponse(200, user);
} catch (Exception e) {
log.error("User retrieval failed", e);
return createErrorResponse(500, "Internal server error");
}
}
private APIGatewayProxyResponseEvent updateUser(APIGatewayProxyRequestEvent input) {
try {
String userId = extractUserId(input.getPath());
if (userId == null || input.getBody() == null) {
return createErrorResponse(400, "User ID and request body are required");
}
// Authorize access
if (!securityValidator.canModifyUser(input, userId)) {
return createErrorResponse(403, "Access denied to modify user");
}
UpdateUserRequest request = objectMapper.readValue(input.getBody(), UpdateUserRequest.class);
Set<ConstraintViolation<UpdateUserRequest>> violations = securityValidator.validateRequest(request);
if (!violations.isEmpty()) {
return createValidationErrorResponse(violations);
}
User user = userService.updateUser(userId, request);
return createSuccessResponse(200, user);
} catch (Exception e) {
log.error("User update failed", e);
return createErrorResponse(400, "Invalid request format");
}
}
private APIGatewayProxyResponseEvent deleteUser(APIGatewayProxyRequestEvent input) {
try {
String userId = extractUserId(input.getPath());
if (userId == null) {
return createErrorResponse(400, "User ID is required");
}
// Authorize access
if (!securityValidator.canModifyUser(input, userId)) {
return createErrorResponse(403, "Access denied to delete user");
}
userService.deleteUser(userId);
return createSuccessResponse(204, null);
} catch (Exception e) {
log.error("User deletion failed", e);
return createErrorResponse(500, "Internal server error");
}
}
private String extractUserId(String path) {
// Extract user ID from path like "/users/123"
if (path == null || !path.startsWith("/users/")) {
return null;
}
return path.substring("/users/".length());
}
private APIGatewayProxyResponseEvent createSuccessResponse(int statusCode, Object body) {
try {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(statusCode);
response.setHeaders(createSecurityHeaders());
if (body != null) {
response.setBody(objectMapper.writeValueAsString(body));
}
return response;
} catch (Exception e) {
log.error("Failed to create success response", e);
return createErrorResponse(500, "Response creation failed");
}
}
private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(statusCode);
response.setHeaders(createSecurityHeaders());
Map<String, String> errorBody = new HashMap<>();
errorBody.put("error", message);
errorBody.put("timestamp", Instant.now().toString());
try {
response.setBody(objectMapper.writeValueAsString(errorBody));
} catch (Exception e) {
log.error("Failed to serialize error response", e);
}
return response;
}
private APIGatewayProxyResponseEvent createValidationErrorResponse(Set<? extends ConstraintViolation<?>> violations) {
List<String> errors = violations.stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.toList());
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("error", "Validation failed");
errorBody.put("details", errors);
errorBody.put("timestamp", Instant.now().toString());
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(400);
response.setHeaders(createSecurityHeaders());
try {
response.setBody(objectMapper.writeValueAsString(errorBody));
} catch (Exception e) {
log.error("Failed to serialize validation error response", e);
}
return response;
}
private Map<String, String> createSecurityHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Content-Type-Options", "nosniff");
headers.put("X-Frame-Options", "DENY");
headers.put("X-XSS-Protection", "1; mode=block");
headers.put("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
return headers;
}
}
2. File Processing Handler
@Component
@Slf4j
public class FileProcessingHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final S3Client s3Client;
private final EncryptionService encryptionService;
private final SecurityValidator securityValidator;
private final ObjectMapper objectMapper;
private final String bucketName;
public FileProcessingHandler(S3Client s3Client,
EncryptionService encryptionService,
SecurityValidator securityValidator,
ObjectMapper objectMapper) {
this.s3Client = s3Client;
this.encryptionService = encryptionService;
this.securityValidator = securityValidator;
this.objectMapper = objectMapper;
this.bucketName = System.getenv("SECURE_BUCKET_NAME");
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
try {
log.info("Processing file request: {} {}", input.getHttpMethod(), input.getPath());
// Validate IAM authorization
if (!securityValidator.validateIamAuthorization(input)) {
return createErrorResponse(403, "Access denied");
}
String httpMethod = input.getHttpMethod();
switch (httpMethod) {
case "POST":
return uploadFile(input);
case "GET":
return downloadFile(input);
case "DELETE":
return deleteFile(input);
default:
return createErrorResponse(405, "Method not allowed");
}
} catch (Exception e) {
log.error("File processing failed", e);
return createErrorResponse(500, "Internal server error");
}
}
private APIGatewayProxyResponseEvent uploadFile(APIGatewayProxyRequestEvent input) {
try {
if (input.getBody() == null) {
return createErrorResponse(400, "File content is required");
}
String fileName = input.getQueryStringParameters() != null ? 
input.getQueryStringParameters().get("fileName") : null;
if (fileName == null || fileName.trim().isEmpty()) {
return createErrorResponse(400, "File name is required");
}
// Validate file name to prevent path traversal
if (!securityValidator.isValidFileName(fileName)) {
return createErrorResponse(400, "Invalid file name");
}
// Encrypt file content
String encryptedContent = encryptionService.encryptData(input.getBody());
// Upload to S3
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key("uploads/" + fileName)
.sseAlgorithm(ServerSideEncryption.AES256)
.metadata(Map.of(
"uploaded-by", securityValidator.getCallerIdentity(input),
"uploaded-at", Instant.now().toString(),
"encrypted", "true"
))
.build();
s3Client.putObject(putRequest, RequestBody.fromString(encryptedContent));
Map<String, String> response = new HashMap<>();
response.put("message", "File uploaded successfully");
response.put("fileName", fileName);
response.put("uploadedAt", Instant.now().toString());
return createSuccessResponse(201, response);
} catch (Exception e) {
log.error("File upload failed", e);
return createErrorResponse(500, "File upload failed");
}
}
private APIGatewayProxyResponseEvent downloadFile(APIGatewayProxyRequestEvent input) {
try {
String fileName = input.getQueryStringParameters() != null ? 
input.getQueryStringParameters().get("fileName") : null;
if (fileName == null || fileName.trim().isEmpty()) {
return createErrorResponse(400, "File name is required");
}
if (!securityValidator.isValidFileName(fileName)) {
return createErrorResponse(400, "Invalid file name");
}
// Download from S3
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key("uploads/" + fileName)
.build();
ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(getRequest);
String encryptedContent = objectBytes.asUtf8String();
// Decrypt content
String decryptedContent = encryptionService.decryptData(encryptedContent);
Map<String, String> response = new HashMap<>();
response.put("content", decryptedContent);
response.put("fileName", fileName);
return createSuccessResponse(200, response);
} catch (NoSuchKeyException e) {
return createErrorResponse(404, "File not found");
} catch (Exception e) {
log.error("File download failed", e);
return createErrorResponse(500, "File download failed");
}
}
private APIGatewayProxyResponseEvent deleteFile(APIGatewayProxyRequestEvent input) {
try {
String fileName = input.getQueryStringParameters() != null ? 
input.getQueryStringParameters().get("fileName") : null;
if (fileName == null || fileName.trim().isEmpty()) {
return createErrorResponse(400, "File name is required");
}
if (!securityValidator.isValidFileName(fileName)) {
return createErrorResponse(400, "Invalid file name");
}
// Check if user has permission to delete this file
if (!securityValidator.canModifyFile(input, fileName)) {
return createErrorResponse(403, "Access denied to delete file");
}
// Delete from S3
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
.bucket(bucketName)
.key("uploads/" + fileName)
.build();
s3Client.deleteObject(deleteRequest);
Map<String, String> response = new HashMap<>();
response.put("message", "File deleted successfully");
response.put("fileName", fileName);
return createSuccessResponse(200, response);
} catch (Exception e) {
log.error("File deletion failed", e);
return createErrorResponse(500, "File deletion failed");
}
}
private APIGatewayProxyResponseEvent createSuccessResponse(int statusCode, Object body) {
try {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(statusCode);
response.setHeaders(createSecurityHeaders());
if (body != null) {
response.setBody(objectMapper.writeValueAsString(body));
}
return response;
} catch (Exception e) {
log.error("Failed to create success response", e);
return createErrorResponse(500, "Response creation failed");
}
}
private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(statusCode);
response.setHeaders(createSecurityHeaders());
Map<String, String> errorBody = new HashMap<>();
errorBody.put("error", message);
errorBody.put("timestamp", Instant.now().toString());
try {
response.setBody(objectMapper.writeValueAsString(errorBody));
} catch (Exception e) {
log.error("Failed to serialize error response", e);
}
return response;
}
private Map<String, String> createSecurityHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Content-Type-Options", "nosniff");
headers.put("X-Frame-Options", "DENY");
headers.put("X-XSS-Protection", "1; mode=block");
return headers;
}
}
3. Security Validator
@Service
@Slf4j
public class SecurityValidator {
private final ObjectMapper objectMapper;
private final Validator validator;
public SecurityValidator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
public boolean validateIamAuthorization(APIGatewayProxyRequestEvent request) {
try {
Map<String, String> identity = request.getRequestContext().getIdentity();
// Check if request is from API Gateway
if (identity == null || identity.get("userArn") == null) {
log.warn("Missing identity information in request");
return false;
}
String userArn = identity.get("userArn");
String accessKey = identity.get("accessKey");
// Validate ARN format
if (!isValidArn(userArn)) {
log.warn("Invalid ARN format: {}", userArn);
return false;
}
// Check if request is from a valid source
if (!isValidSource(identity)) {
log.warn("Invalid request source: {}", userArn);
return false;
}
log.info("IAM authorization validated for: {}", userArn);
return true;
} catch (Exception e) {
log.error("IAM authorization validation failed", e);
return false;
}
}
public boolean canAccessUser(APIGatewayProxyRequestEvent request, String targetUserId) {
try {
Map<String, String> identity = request.getRequestContext().getIdentity();
String callerArn = identity.get("userArn");
// Extract user ID from ARN (assuming IAM user name is the user ID)
String callerUserId = extractUserIdFromArn(callerArn);
// User can access their own data
if (callerUserId != null && callerUserId.equals(targetUserId)) {
return true;
}
// Check if caller has admin privileges
if (hasAdminPrivileges(identity)) {
return true;
}
log.warn("Access denied: {} cannot access user {}", callerArn, targetUserId);
return false;
} catch (Exception e) {
log.error("User access validation failed", e);
return false;
}
}
public boolean canModifyUser(APIGatewayProxyRequestEvent request, String targetUserId) {
try {
Map<String, String> identity = request.getRequestContext().getIdentity();
String callerArn = identity.get("userArn");
// Only allow users to modify their own data or admins
String callerUserId = extractUserIdFromArn(callerArn);
if (callerUserId != null && callerUserId.equals(targetUserId)) {
return true;
}
if (hasAdminPrivileges(identity)) {
return true;
}
log.warn("Modification denied: {} cannot modify user {}", callerArn, targetUserId);
return false;
} catch (Exception e) {
log.error("User modification validation failed", e);
return false;
}
}
public boolean canModifyFile(APIGatewayProxyRequestEvent request, String fileName) {
try {
// In a real implementation, you would check file ownership/metadata
// For now, allow users to modify files they uploaded
Map<String, String> identity = request.getRequestContext().getIdentity();
return hasAdminPrivileges(identity); // Only admins can delete files for now
} catch (Exception e) {
log.error("File modification validation failed", e);
return false;
}
}
public boolean isValidFileName(String fileName) {
// Prevent path traversal attacks
if (fileName == null || fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
return false;
}
// Validate file name length and characters
if (fileName.length() > 255 || !fileName.matches("^[a-zA-Z0-9._-]+$")) {
return false;
}
return true;
}
public String getCallerIdentity(APIGatewayProxyRequestEvent request) {
Map<String, String> identity = request.getRequestContext().getIdentity();
return identity != null ? identity.get("userArn") : "unknown";
}
public <T> Set<ConstraintViolation<T>> validateRequest(T request) {
return validator.validate(request);
}
private boolean isValidArn(String arn) {
return arn != null && arn.startsWith("arn:aws:");
}
private boolean isValidSource(Map<String, String> identity) {
String userAgent = identity.get("userAgent");
String sourceIp = identity.get("sourceIp");
// Validate source IP (example: allow only from specific ranges)
if (sourceIp != null && isPrivateIp(sourceIp)) {
log.warn("Request from private IP: {}", sourceIp);
return false;
}
// Validate user agent
if (userAgent != null && userAgent.contains("MaliciousBot")) {
log.warn("Request from malicious user agent: {}", userAgent);
return false;
}
return true;
}
private boolean isPrivateIp(String ip) {
// Check if IP is in private range
return ip.startsWith("10.") || 
ip.startsWith("192.168.") || 
(ip.startsWith("172.") && isInPrivateRange(ip));
}
private boolean isInPrivateRange(String ip) {
try {
String[] parts = ip.split("\\.");
if (parts.length >= 2) {
int secondOctet = Integer.parseInt(parts[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
} catch (NumberFormatException e) {
// Ignore parsing errors
}
return false;
}
private String extractUserIdFromArn(String arn) {
if (arn == null) return null;
// Extract user name from ARN: arn:aws:iam::123456789012:user/username
String[] parts = arn.split("/");
return parts.length > 1 ? parts[1] : null;
}
private boolean hasAdminPrivileges(Map<String, String> identity) {
String userArn = identity.get("userArn");
// Check if user is in admin group (simplified)
// In real implementation, you would check IAM policies
return userArn != null && userArn.contains("Admin");
}
}

Domain Models

1. Request/Response Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "Username can only contain letters, numbers, underscores, and hyphens")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters long")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$", 
message = "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character")
private String password;
@NotBlank(message = "First name is required")
@Size(max = 100, message = "First name cannot exceed 100 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(max = 100, message = "Last name cannot exceed 100 characters")
private String lastName;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateUserRequest {
@Email(message = "Email must be valid")
private String email;
@Size(max = 100, message = "First name cannot exceed 100 characters")
private String firstName;
@Size(max = 100, message = "Last name cannot exceed 100 characters")
private String lastName;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String userId;
private String username;
private String email;
private String firstName;
private String lastName;
private Instant createdAt;
private Instant updatedAt;
private boolean active;
private List<String> roles;
}
2. User Service
@Service
@Slf4j
public class UserService {
private final DynamoDbClient dynamoDbClient;
private final PasswordEncoder passwordEncoder;
private final EncryptionService encryptionService;
private final String tableName;
public UserService(DynamoDbClient dynamoDbClient,
PasswordEncoder passwordEncoder,
EncryptionService encryptionService) {
this.dynamoDbClient = dynamoDbClient;
this.passwordEncoder = passwordEncoder;
this.encryptionService = encryptionService;
this.tableName = System.getenv("USERS_TABLE_NAME");
}
public User createUser(CreateUserRequest request) {
try {
// Check if user already exists
if (userExists(request.getUsername(), request.getEmail())) {
throw new RuntimeException("User already exists");
}
String userId = UUID.randomUUID().toString();
Instant now = Instant.now();
// Hash password
String hashedPassword = passwordEncoder.encode(request.getPassword());
// Encrypt sensitive data
String encryptedEmail = encryptionService.encryptData(request.getEmail());
// Create user item
Map<String, AttributeValue> item = new HashMap<>();
item.put("userId", AttributeValue.builder().s(userId).build());
item.put("username", AttributeValue.builder().s(request.getUsername()).build());
item.put("email", AttributeValue.builder().s(encryptedEmail).build());
item.put("firstName", AttributeValue.builder().s(request.getFirstName()).build());
item.put("lastName", AttributeValue.builder().s(request.getLastName()).build());
item.put("passwordHash", AttributeValue.builder().s(hashedPassword).build());
item.put("createdAt", AttributeValue.builder().s(now.toString()).build());
item.put("updatedAt", AttributeValue.builder().s(now.toString()).build());
item.put("active", AttributeValue.builder().bool(true).build());
item.put("roles", AttributeValue.builder().ss("USER").build());
PutItemRequest putRequest = PutItemRequest.builder()
.tableName(tableName)
.item(item)
.build();
dynamoDbClient.putItem(putRequest);
log.info("User created successfully: {}", userId);
return User.builder()
.userId(userId)
.username(request.getUsername())
.email(request.getEmail()) // Return decrypted email
.firstName(request.getFirstName())
.lastName(request.getLastName())
.createdAt(now)
.updatedAt(now)
.active(true)
.roles(List.of("USER"))
.build();
} catch (Exception e) {
log.error("Failed to create user", e);
throw new RuntimeException("User creation failed", e);
}
}
public User getUser(String userId) {
try {
GetItemRequest getRequest = GetItemRequest.builder()
.tableName(tableName)
.key(Map.of("userId", AttributeValue.builder().s(userId).build()))
.build();
GetItemResponse response = dynamoDbClient.getItem(getRequest);
if (response.item() == null || response.item().isEmpty()) {
return null;
}
return mapToUser(response.item());
} catch (Exception e) {
log.error("Failed to get user: {}", userId, e);
throw new RuntimeException("User retrieval failed", e);
}
}
public User updateUser(String userId, UpdateUserRequest request) {
try {
// Build update expression
StringBuilder updateExpression = new StringBuilder("SET updatedAt = :updatedAt");
Map<String, String> expressionAttributeNames = new HashMap<>();
Map<String, AttributeValue> expressionAttributeValues = new HashMap<>();
expressionAttributeValues.put(":updatedAt", 
AttributeValue.builder().s(Instant.now().toString()).build());
if (request.getEmail() != null) {
updateExpression.append(", email = :email");
expressionAttributeValues.put(":email", 
AttributeValue.builder().s(encryptionService.encryptData(request.getEmail())).build());
}
if (request.getFirstName() != null) {
updateExpression.append(", firstName = :firstName");
expressionAttributeValues.put(":firstName", 
AttributeValue.builder().s(request.getFirstName()).build());
}
if (request.getLastName() != null) {
updateExpression.append(", lastName = :lastName");
expressionAttributeValues.put(":lastName", 
AttributeValue.builder().s(request.getLastName()).build());
}
UpdateItemRequest updateRequest = UpdateItemRequest.builder()
.tableName(tableName)
.key(Map.of("userId", AttributeValue.builder().s(userId).build()))
.updateExpression(updateExpression.toString())
.expressionAttributeValues(expressionAttributeValues)
.returnValues(ReturnValue.ALL_NEW)
.build();
UpdateItemResponse response = dynamoDbClient.updateItem(updateRequest);
return mapToUser(response.attributes());
} catch (Exception e) {
log.error("Failed to update user: {}", userId, e);
throw new RuntimeException("User update failed", e);
}
}
public void deleteUser(String userId) {
try {
DeleteItemRequest deleteRequest = DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of("userId", AttributeValue.builder().s(userId).build()))
.build();
dynamoDbClient.deleteItem(deleteRequest);
log.info("User deleted successfully: {}", userId);
} catch (Exception e) {
log.error("Failed to delete user: {}", userId, e);
throw new RuntimeException("User deletion failed", e);
}
}
private boolean userExists(String username, String email) {
try {
// Check by username
QueryRequest queryRequest = QueryRequest.builder()
.tableName(tableName)
.indexName("UsernameIndex")
.keyConditionExpression("username = :username")
.expressionAttributeValues(Map.of(
":username", AttributeValue.builder().s(username).build()
))
.build();
QueryResponse response = dynamoDbClient.query(queryRequest);
if (response.count() > 0) {
return true;
}
// Check by email (encrypted)
String encryptedEmail = encryptionService.encryptData(email);
QueryRequest emailQueryRequest = QueryRequest.builder()
.tableName(tableName)
.indexName("EmailIndex")
.keyConditionExpression("email = :email")
.expressionAttributeValues(Map.of(
":email", AttributeValue.builder().s(encryptedEmail).build()
))
.build();
QueryResponse emailResponse = dynamoDbClient.query(emailQueryRequest);
return emailResponse.count() > 0;
} catch (Exception e) {
log.error("Failed to check user existence", e);
throw new RuntimeException("User existence check failed", e);
}
}
private User mapToUser(Map<String, AttributeValue> item) {
try {
String encryptedEmail = item.get("email").s();
String decryptedEmail = encryptionService.decryptData(encryptedEmail);
return User.builder()
.userId(item.get("userId").s())
.username(item.get("username").s())
.email(decryptedEmail)
.firstName(item.get("firstName").s())
.lastName(item.get("lastName").s())
.createdAt(Instant.parse(item.get("createdAt").s()))
.updatedAt(Instant.parse(item.get("updatedAt").s()))
.active(item.get("active").bool())
.roles(item.get("roles").ss())
.build();
} catch (Exception e) {
log.error("Failed to map DynamoDB item to User", e);
throw new RuntimeException("User mapping failed", e);
}
}
}

Security Monitoring and Auditing

1. Security Audit Service
@Service
@Slf4j
public class SecurityAuditService {
private final CloudWatchLogsClient cloudWatchLogsClient;
private final String logGroupName;
public SecurityAuditService(CloudWatchLogsClient cloudWatchLogsClient) {
this.cloudWatchLogsClient = cloudWatchLogsClient;
this.logGroupName = "/aws/lambda/secure-sam-app";
}
public void logSecurityEvent(SecurityEvent event) {
try {
PutLogEventsRequest request = PutLogEventsRequest.builder()
.logGroupName(logGroupName)
.logStreamName("security-events")
.logEvents(InputLogEvent.builder()
.message(event.toJson())
.timestamp(System.currentTimeMillis())
.build())
.build();
cloudWatchLogsClient.putLogEvents(request);
} catch (Exception e) {
log.error("Failed to log security event", e);
// Fallback to standard logging
log.warn("Security Event: {}", event.toJson());
}
}
public void logApiAccess(APIGatewayProxyRequestEvent request, int statusCode) {
SecurityEvent event = SecurityEvent.builder()
.eventType("API_ACCESS")
.timestamp(Instant.now())
.principal(getCallerIdentity(request))
.resource(request.getPath())
.action(request.getHttpMethod())
.status(statusCode >= 200 && statusCode < 300 ? "SUCCESS" : "FAILED")
.details(Map.of(
"userAgent", request.getRequestContext().getIdentity().get("userAgent"),
"sourceIp", request.getRequestContext().getIdentity().get("sourceIp"),
"statusCode", String.valueOf(statusCode)
))
.build();
logSecurityEvent(event);
}
public void logSecurityViolation(APIGatewayProxyRequestEvent request, String violationType) {
SecurityEvent event = SecurityEvent.builder()
.eventType("SECURITY_VIOLATION")
.timestamp(Instant.now())
.principal(getCallerIdentity(request))
.resource(request.getPath())
.action(request.getHttpMethod())
.status("BLOCKED")
.details(Map.of(
"violationType", violationType,
"userAgent", request.getRequestContext().getIdentity().get("userAgent"),
"sourceIp", request.getRequestContext().getIdentity().get("sourceIp")
))
.build();
logSecurityEvent(event);
}
private String getCallerIdentity(APIGatewayProxyRequestEvent request) {
Map<String, String> identity = request.getRequestContext().getIdentity();
return identity != null ? identity.get("userArn") : "unknown";
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class SecurityEvent {
private String eventType;
private Instant timestamp;
private String principal;
private String resource;
private String action;
private String status;
private Map<String, String> details;
public String toJson() {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper.writeValueAsString(this);
} catch (Exception e) {
return "{\"error\": \"Failed to serialize security event\"}";
}
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class UserManagementHandlerTest {
@Mock
private UserService userService;
@Mock
private SecurityValidator securityValidator;
@InjectMocks
private UserManagementHandler handler;
private ObjectMapper objectMapper = new ObjectMapper();
@Test
void testCreateUser_Success() throws Exception {
// Setup
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email("[email protected]")
.password("SecurePass123!")
.firstName("Test")
.lastName("User")
.build();
User user = User.builder()
.userId("123")
.username("testuser")
.email("[email protected]")
.build();
APIGatewayProxyRequestEvent apiRequest = new APIGatewayProxyRequestEvent();
apiRequest.setHttpMethod("POST");
apiRequest.setPath("/users");
apiRequest.setBody(objectMapper.writeValueAsString(request));
when(securityValidator.validateIamAuthorization(any())).thenReturn(true);
when(securityValidator.validateRequest(any())).thenReturn(Collections.emptySet());
when(userService.createUser(any())).thenReturn(user);
// Execute
APIGatewayProxyResponseEvent response = handler.handleRequest(apiRequest, null);
// Verify
assertEquals(201, response.getStatusCode());
verify(userService).createUser(any());
}
@Test
void testCreateUser_Unauthorized() {
// Setup
APIGatewayProxyRequestEvent apiRequest = new APIGatewayProxyRequestEvent();
apiRequest.setHttpMethod("POST");
apiRequest.setPath("/users");
when(securityValidator.validateIamAuthorization(any())).thenReturn(false);
// Execute
APIGatewayProxyResponseEvent response = handler.handleRequest(apiRequest, null);
// Verify
assertEquals(403, response.getStatusCode());
verify(userService, never()).createUser(any());
}
}
@SpringBootTest
class SecurityValidatorTest {
@Autowired
private SecurityValidator securityValidator;
@Test
void testValidFileName() {
assertTrue(securityValidator.isValidFileName("document.pdf"));
assertTrue(securityValidator.isValidFileName("image_123.jpg"));
}
@Test
void testInvalidFileName() {
assertFalse(securityValidator.isValidFileName("../etc/passwd"));
assertFalse(securityValidator.isValidFileName("file/with/slashes.txt"));
assertFalse(securityValidator.isValidFileName("file\\with\\backslashes.txt"));
}
}
2. Integration Tests
@SpringBootTest
@TestPropertySource(properties = {
"app.security.kms-key-id=test-key",
"app.security.secrets-prefix=/test/app"
})
class SecureSamApplicationTest {
@Autowired
private UserManagementHandler userManagementHandler;
@Test
void testApplicationContextLoads() {
assertNotNull(userManagementHandler);
}
}

Best Practices

  1. IAM Security:
  • Principle of least privilege
  • Use resource-based policies where possible
  • Regular permission reviews
  1. Data Protection:
  • Encrypt sensitive data at rest and in transit
  • Use AWS KMS for key management
  • Implement proper key rotation
  1. Network Security:
  • Use VPC for Lambda functions when needed
  • Implement security groups and NACLs
  • Use private subnets for databases
  1. Monitoring & Auditing:
  • Enable CloudTrail for API logging
  • Use CloudWatch for monitoring
  • Implement custom security events
  1. Application Security:
  • Input validation and sanitization
  • Secure headers implementation
  • Proper error handling without information leakage
// Example of secure environment configuration
@Component
public class SecureEnvironmentConfig {
public void validateEnvironment() {
validateRequiredVars("USERS_TABLE_NAME", "SECURE_BUCKET_NAME");
validateKmsKey();
validateSecretsManager();
}
private void validateRequiredVars(String... vars) {
for (String var : vars) {
if (System.getenv(var) == null) {
throw new IllegalStateException("Required environment variable missing: " + var);
}
}
}
}

Conclusion

This AWS SAM security implementation provides:

  • Comprehensive IAM security with least privilege principles
  • End-to-end encryption for data protection
  • Secure API Gateway configuration with IAM authorization
  • Proper secret management using AWS Secrets Manager
  • Security monitoring and auditing capabilities
  • Input validation and sanitization at all layers

The solution follows AWS security best practices and can be extended with additional security measures like WAF, Shield, and GuardDuty for enterprise-grade security in serverless applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper