docs
reference
services
tessl install tessl/maven-com-pulumi--aws@7.16.0Pulumi Java SDK for AWS providing strongly-typed Infrastructure-as-Code for 227 AWS service packages including compute, storage, databases, networking, security, analytics, machine learning, and more.
This comprehensive guide demonstrates security best practices for AWS infrastructure using Pulumi. Implement defense-in-depth with encryption, access controls, monitoring, and compliance.
┌──────────────────────────────────────────────────────┐
│ Identity & Access Management │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ IAM Roles │ │ IAM Users │ │ IAM Groups │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└──────────────────────┬───────────────────────────────┘
│
┌──────────────────────┴───────────────────────────────┐
│ Encryption Layer │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ KMS Keys │ │ Secrets │ │ ACM │ │
│ │ │ │ Manager │ │ Certificates │ │
│ └────────────┘ └─────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│
┌──────────────────────┴───────────────────────────────┐
│ Network Security Layer │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Security │ │ NACLs │ │ WAF Rules │ │
│ │ Groups │ │ │ │ │ │
│ └────────────┘ └─────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│
┌──────────────────────┴───────────────────────────────┐
│ Audit & Compliance │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ CloudTrail │ │ Config │ │ GuardDuty │ │
│ │ │ │ Rules │ │ │ │
│ └────────────┘ └─────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────┘import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Configuration
const config = new pulumi.Config();
const projectName = "secure-app";
const environment = pulumi.getStack();
// Tags for all resources
const tags = {
Environment: environment,
Project: projectName,
ManagedBy: "pulumi",
SecurityLevel: "high",
};
// ============================================================================
// 1. KMS ENCRYPTION KEYS
// ============================================================================
// Create customer managed KMS key for application data
const appKmsKey = new aws.kms.Key("app-key", {
description: `${projectName} application encryption key`,
deletionWindowInDays: 30,
enableKeyRotation: true,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "Enable IAM User Permissions",
Effect: "Allow",
Principal: {
AWS: pulumi.interpolate`arn:aws:iam::${aws.getCallerIdentityOutput().accountId}:root`,
},
Action: "kms:*",
Resource: "*",
},
{
Sid: "Allow CloudWatch Logs",
Effect: "Allow",
Principal: {
Service: `logs.${aws.getRegionOutput().name}.amazonaws.com`,
},
Action: [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:CreateGrant",
"kms:DescribeKey",
],
Resource: "*",
Condition: {
ArnLike: {
"kms:EncryptionContext:aws:logs:arn": pulumi.interpolate`arn:aws:logs:${aws.getRegionOutput().name}:${aws.getCallerIdentityOutput().accountId}:*`,
},
},
},
],
}),
tags,
});
// Create alias for easy reference
new aws.kms.Alias("app-key-alias", {
name: `alias/${projectName}-app`,
targetKeyId: appKmsKey.id,
});
// Create separate KMS key for S3 data
const s3KmsKey = new aws.kms.Key("s3-key", {
description: `${projectName} S3 encryption key`,
deletionWindowInDays: 30,
enableKeyRotation: true,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "Enable IAM User Permissions",
Effect: "Allow",
Principal: {
AWS: pulumi.interpolate`arn:aws:iam::${aws.getCallerIdentityOutput().accountId}:root`,
},
Action: "kms:*",
Resource: "*",
},
{
Sid: "Allow S3 to use the key",
Effect: "Allow",
Principal: {
Service: "s3.amazonaws.com",
},
Action: [
"kms:Decrypt",
"kms:GenerateDataKey",
],
Resource: "*",
},
],
}),
tags,
});
new aws.kms.Alias("s3-key-alias", {
name: `alias/${projectName}-s3`,
targetKeyId: s3KmsKey.id,
});
// Create KMS key for RDS
const rdsKmsKey = new aws.kms.Key("rds-key", {
description: `${projectName} RDS encryption key`,
deletionWindowInDays: 30,
enableKeyRotation: true,
tags,
});
new aws.kms.Alias("rds-key-alias", {
name: `alias/${projectName}-rds`,
targetKeyId: rdsKmsKey.id,
});
// ============================================================================
// 2. SECRETS MANAGER
// ============================================================================
// Store database credentials in Secrets Manager
const dbSecret = new aws.secretsmanager.Secret("db-credentials", {
name: `${projectName}/db/credentials`,
description: "Database credentials",
kmsKeyId: appKmsKey.id,
recoveryWindowInDays: 30,
tags,
});
// Generate random password
const randomPassword = new aws.secretsmanager.SecretVersion("db-password-version", {
secretId: dbSecret.id,
secretString: JSON.stringify({
username: "dbadmin",
password: config.requireSecret("dbPassword"),
engine: "postgres",
host: "", // Will be updated after RDS creation
port: 5432,
dbname: "appdb",
}),
});
// Store API keys
const apiKeySecret = new aws.secretsmanager.Secret("api-keys", {
name: `${projectName}/api/keys`,
description: "Third-party API keys",
kmsKeyId: appKmsKey.id,
recoveryWindowInDays: 30,
tags,
});
new aws.secretsmanager.SecretVersion("api-keys-version", {
secretId: apiKeySecret.id,
secretString: JSON.stringify({
stripe: config.getSecret("stripeApiKey") || "placeholder",
sendgrid: config.getSecret("sendgridApiKey") || "placeholder",
}),
});
// Enable automatic rotation for database credentials
const rotationLambdaRole = new aws.iam.Role("rotation-lambda-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "lambda.amazonaws.com",
},
}],
}),
tags,
});
// ============================================================================
// 3. IAM ROLES AND POLICIES
// ============================================================================
// Application role for EC2/ECS with least privilege
const appRole = new aws.iam.Role("app-role", {
name: `${projectName}-app-role`,
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: ["ec2.amazonaws.com", "ecs-tasks.amazonaws.com"],
},
},
],
}),
maxSessionDuration: 3600, // 1 hour
tags,
});
// Custom policy with specific permissions
const appPolicy = new aws.iam.Policy("app-policy", {
name: `${projectName}-app-policy`,
description: "Application permissions with least privilege",
policy: pulumi.all([appKmsKey.arn, s3KmsKey.arn, dbSecret.arn, apiKeySecret.arn]).apply(
([appKeyArn, s3KeyArn, dbSecretArn, apiSecretArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "ReadSecretsManager",
Effect: "Allow",
Action: [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
],
Resource: [dbSecretArn, apiSecretArn],
},
{
Sid: "DecryptSecrets",
Effect: "Allow",
Action: [
"kms:Decrypt",
"kms:DescribeKey",
],
Resource: [appKeyArn],
Condition: {
StringEquals: {
"kms:ViaService": `secretsmanager.${aws.getRegionOutput().name}.amazonaws.com`,
},
},
},
{
Sid: "WriteApplicationLogs",
Effect: "Allow",
Action: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
Resource: pulumi.interpolate`arn:aws:logs:${aws.getRegionOutput().name}:${aws.getCallerIdentityOutput().accountId}:log-group:/app/${projectName}:*`,
},
{
Sid: "ReadWriteS3Data",
Effect: "Allow",
Action: [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
],
Resource: pulumi.interpolate`arn:aws:s3:::${projectName}-data-*/*`,
},
{
Sid: "EncryptS3Objects",
Effect: "Allow",
Action: [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
],
Resource: [s3KeyArn],
Condition: {
StringEquals: {
"kms:ViaService": `s3.${aws.getRegionOutput().name}.amazonaws.com`,
},
},
},
],
})
),
tags,
});
// Attach policy to role
new aws.iam.RolePolicyAttachment("app-policy-attachment", {
role: appRole.name,
policyArn: appPolicy.arn,
});
// Create instance profile for EC2
const appInstanceProfile = new aws.iam.InstanceProfile("app-instance-profile", {
name: `${projectName}-instance-profile`,
role: appRole.name,
});
// Lambda execution role with restricted permissions
const lambdaRole = new aws.iam.Role("lambda-role", {
name: `${projectName}-lambda-role`,
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "lambda.amazonaws.com",
},
}],
}),
tags,
});
// Attach managed policy for Lambda execution
new aws.iam.RolePolicyAttachment("lambda-basic-execution", {
role: lambdaRole.name,
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
});
// Cross-account access role (for external services)
const crossAccountRole = new aws.iam.Role("cross-account-role", {
name: `${projectName}-cross-account`,
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: {
AWS: config.require("trustedAccountId"),
},
Action: "sts:AssumeRole",
Condition: {
StringEquals: {
"sts:ExternalId": config.requireSecret("externalId"),
},
},
}],
}),
tags,
});
// ============================================================================
// 4. SECURITY GROUPS WITH LEAST PRIVILEGE
// ============================================================================
// VPC for secure networking
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { ...tags, Name: `${projectName}-vpc` },
});
// Enable VPC Flow Logs
const flowLogsRole = new aws.iam.Role("flow-logs-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "vpc-flow-logs.amazonaws.com",
},
}],
}),
tags,
});
const flowLogsPolicy = new aws.iam.RolePolicy("flow-logs-policy", {
role: flowLogsRole.id,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
],
Resource: "*",
}],
}),
});
const flowLogsGroup = new aws.cloudwatch.LogGroup("vpc-flow-logs", {
name: `/aws/vpc/${projectName}`,
retentionInDays: 90,
kmsKeyId: appKmsKey.arn,
tags,
});
const vpcFlowLog = new aws.ec2.FlowLog("vpc-flow-log", {
vpcId: vpc.id,
trafficType: "ALL",
logDestinationType: "cloud-watch-logs",
logDestination: flowLogsGroup.arn,
iamRoleArn: flowLogsRole.arn,
tags,
}, { dependsOn: [flowLogsPolicy] });
// Application Load Balancer Security Group
const albSg = new aws.ec2.SecurityGroup("alb-sg", {
name: `${projectName}-alb-sg`,
description: "Security group for Application Load Balancer",
vpcId: vpc.id,
tags: { ...tags, Name: `${projectName}-alb-sg`, Layer: "public" },
});
// Allow HTTPS only from internet
new aws.ec2.SecurityGroupRule("alb-ingress-https", {
type: "ingress",
securityGroupId: albSg.id,
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow HTTPS from internet",
});
// Redirect HTTP to HTTPS
new aws.ec2.SecurityGroupRule("alb-ingress-http", {
type: "ingress",
securityGroupId: albSg.id,
protocol: "tcp",
fromPort: 80,
toPort: 80,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow HTTP for redirect to HTTPS",
});
// Egress to application tier only
new aws.ec2.SecurityGroupRule("alb-egress", {
type: "egress",
securityGroupId: albSg.id,
protocol: "tcp",
fromPort: 8080,
toPort: 8080,
sourceSecurityGroupId: "", // Will reference app SG
description: "Allow traffic to application tier",
});
// Application Security Group
const appSg = new aws.ec2.SecurityGroup("app-sg", {
name: `${projectName}-app-sg`,
description: "Security group for application tier",
vpcId: vpc.id,
tags: { ...tags, Name: `${projectName}-app-sg`, Layer: "application" },
});
// Allow traffic from ALB only
new aws.ec2.SecurityGroupRule("app-ingress-alb", {
type: "ingress",
securityGroupId: appSg.id,
protocol: "tcp",
fromPort: 8080,
toPort: 8080,
sourceSecurityGroupId: albSg.id,
description: "Allow traffic from ALB only",
});
// Allow HTTPS for external API calls
new aws.ec2.SecurityGroupRule("app-egress-https", {
type: "egress",
securityGroupId: appSg.id,
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow HTTPS for external APIs",
});
// Database Security Group
const dbSg = new aws.ec2.SecurityGroup("db-sg", {
name: `${projectName}-db-sg`,
description: "Security group for database tier",
vpcId: vpc.id,
tags: { ...tags, Name: `${projectName}-db-sg`, Layer: "database" },
});
// Allow PostgreSQL from application tier only
new aws.ec2.SecurityGroupRule("db-ingress-app", {
type: "ingress",
securityGroupId: dbSg.id,
protocol: "tcp",
fromPort: 5432,
toPort: 5432,
sourceSecurityGroupId: appSg.id,
description: "Allow PostgreSQL from application tier only",
});
// No egress rules (database doesn't need outbound)
new aws.ec2.SecurityGroupRule("db-no-egress", {
type: "egress",
securityGroupId: dbSg.id,
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["127.0.0.1/32"],
description: "Deny all egress",
});
// ============================================================================
// 5. S3 BUCKET SECURITY
// ============================================================================
// Secure S3 bucket with encryption and access controls
const secureBucket = new aws.s3.BucketV2("secure-bucket", {
bucket: `${projectName}-data-${aws.getCallerIdentityOutput().accountId}`,
tags,
});
// Enable versioning
new aws.s3.BucketVersioningV2("secure-versioning", {
bucket: secureBucket.id,
versioningConfiguration: {
status: "Enabled",
},
});
// Block all public access
new aws.s3.BucketPublicAccessBlock("secure-public-access-block", {
bucket: secureBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// Enable encryption with KMS
new aws.s3.BucketServerSideEncryptionConfigurationV2("secure-encryption", {
bucket: secureBucket.id,
rules: [{
applyServerSideEncryptionByDefault: {
sseAlgorithm: "aws:kms",
kmsMasterKeyId: s3KmsKey.id,
},
bucketKeyEnabled: true,
}],
});
// Enable logging
const logBucket = new aws.s3.BucketV2("log-bucket", {
bucket: `${projectName}-logs-${aws.getCallerIdentityOutput().accountId}`,
tags,
});
new aws.s3.BucketPublicAccessBlock("log-public-access-block", {
bucket: logBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
new aws.s3.BucketLoggingV2("secure-logging", {
bucket: secureBucket.id,
targetBucket: logBucket.id,
targetPrefix: "s3-access-logs/",
});
// Bucket policy with strict access controls
const secureBucketPolicy = new aws.s3.BucketPolicy("secure-bucket-policy", {
bucket: secureBucket.id,
policy: pulumi.all([secureBucket.arn, appRole.arn]).apply(([bucketArn, roleArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "DenyInsecureTransport",
Effect: "Deny",
Principal: "*",
Action: "s3:*",
Resource: [bucketArn, `${bucketArn}/*`],
Condition: {
Bool: {
"aws:SecureTransport": "false",
},
},
},
{
Sid: "DenyUnencryptedObjectUploads",
Effect: "Deny",
Principal: "*",
Action: "s3:PutObject",
Resource: `${bucketArn}/*`,
Condition: {
StringNotEquals: {
"s3:x-amz-server-side-encryption": "aws:kms",
},
},
},
{
Sid: "AllowApplicationRole",
Effect: "Allow",
Principal: {
AWS: roleArn,
},
Action: [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
],
Resource: `${bucketArn}/*`,
},
],
})
),
});
// Object lock for compliance
new aws.s3.BucketObjectLockConfigurationV2("secure-object-lock", {
bucket: secureBucket.id,
objectLockEnabled: "Enabled",
rule: {
defaultRetention: {
mode: "GOVERNANCE",
days: 30,
},
},
});
// ============================================================================
// 6. CLOUDTRAIL AUDIT LOGGING
// ============================================================================
// S3 bucket for CloudTrail logs
const cloudtrailBucket = new aws.s3.BucketV2("cloudtrail-bucket", {
bucket: `${projectName}-cloudtrail-${aws.getCallerIdentityOutput().accountId}`,
tags,
});
new aws.s3.BucketPublicAccessBlock("cloudtrail-public-access-block", {
bucket: cloudtrailBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// Bucket policy for CloudTrail
const cloudtrailBucketPolicy = new aws.s3.BucketPolicy("cloudtrail-bucket-policy", {
bucket: cloudtrailBucket.id,
policy: pulumi.all([cloudtrailBucket.arn]).apply(([bucketArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "AWSCloudTrailAclCheck",
Effect: "Allow",
Principal: {
Service: "cloudtrail.amazonaws.com",
},
Action: "s3:GetBucketAcl",
Resource: bucketArn,
},
{
Sid: "AWSCloudTrailWrite",
Effect: "Allow",
Principal: {
Service: "cloudtrail.amazonaws.com",
},
Action: "s3:PutObject",
Resource: `${bucketArn}/*`,
Condition: {
StringEquals: {
"s3:x-amz-acl": "bucket-owner-full-control",
},
},
},
],
})
),
});
// CloudWatch Log Group for CloudTrail
const cloudtrailLogGroup = new aws.cloudwatch.LogGroup("cloudtrail-logs", {
name: `/aws/cloudtrail/${projectName}`,
retentionInDays: 365,
kmsKeyId: appKmsKey.arn,
tags,
});
// IAM role for CloudTrail to write to CloudWatch Logs
const cloudtrailRole = new aws.iam.Role("cloudtrail-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "cloudtrail.amazonaws.com",
},
}],
}),
tags,
});
new aws.iam.RolePolicy("cloudtrail-policy", {
role: cloudtrailRole.id,
policy: pulumi.all([cloudtrailLogGroup.arn]).apply(([logGroupArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: [
"logs:CreateLogStream",
"logs:PutLogEvents",
],
Resource: `${logGroupArn}:*`,
}],
})
),
});
// Create CloudTrail
const trail = new aws.cloudtrail.Trail("trail", {
name: `${projectName}-trail`,
s3BucketName: cloudtrailBucket.bucket,
includeGlobalServiceEvents: true,
isMultiRegionTrail: true,
enableLogFileValidation: true,
cloudWatchLogsGroupArn: cloudtrailLogGroup.arn,
cloudWatchLogsRoleArn: cloudtrailRole.arn,
kmsKeyId: appKmsKey.id,
eventSelectors: [{
readWriteType: "All",
includeManagementEvents: true,
dataResources: [
{
type: "AWS::S3::Object",
values: [pulumi.interpolate`${secureBucket.arn}/`],
},
{
type: "AWS::Lambda::Function",
values: ["arn:aws:lambda"],
},
],
}],
tags,
}, { dependsOn: [cloudtrailBucketPolicy] });
// ============================================================================
// 7. AWS CONFIG FOR COMPLIANCE
// ============================================================================
// S3 bucket for Config
const configBucket = new aws.s3.BucketV2("config-bucket", {
bucket: `${projectName}-config-${aws.getCallerIdentityOutput().accountId}`,
tags,
});
new aws.s3.BucketPublicAccessBlock("config-public-access-block", {
bucket: configBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// IAM role for Config
const configRole = new aws.iam.Role("config-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "config.amazonaws.com",
},
}],
}),
managedPolicyArns: [
"arn:aws:iam::aws:policy/service-role/ConfigRole",
],
tags,
});
// Config recorder
const configRecorder = new aws.cfg.Recorder("config-recorder", {
name: `${projectName}-recorder`,
roleArn: configRole.arn,
recordingGroup: {
allSupported: true,
includeGlobalResourceTypes: true,
},
});
// Config delivery channel
const configDeliveryChannel = new aws.cfg.DeliveryChannel("config-delivery", {
name: `${projectName}-delivery`,
s3BucketName: configBucket.bucket,
snapshotDeliveryProperties: {
deliveryFrequency: "TwentyFour_Hours",
},
}, { dependsOn: [configRecorder] });
// Start Config recorder
new aws.cfg.RecorderStatus("config-recorder-status", {
name: configRecorder.name,
isEnabled: true,
}, { dependsOn: [configDeliveryChannel] });
// Config rules for compliance
new aws.cfg.Rule("encrypted-volumes", {
name: "encrypted-volumes",
description: "Check that EBS volumes are encrypted",
source: {
owner: "AWS",
sourceIdentifier: "ENCRYPTED_VOLUMES",
},
}, { dependsOn: [configRecorder] });
new aws.cfg.Rule("s3-bucket-public-read-prohibited", {
name: "s3-bucket-public-read-prohibited",
description: "Check that S3 buckets do not allow public read access",
source: {
owner: "AWS",
sourceIdentifier: "S3_BUCKET_PUBLIC_READ_PROHIBITED",
},
}, { dependsOn: [configRecorder] });
new aws.cfg.Rule("rds-storage-encrypted", {
name: "rds-storage-encrypted",
description: "Check that RDS instances are encrypted",
source: {
owner: "AWS",
sourceIdentifier: "RDS_STORAGE_ENCRYPTED",
},
}, { dependsOn: [configRecorder] });
// ============================================================================
// 8. GUARDDUTY THREAT DETECTION
// ============================================================================
// Enable GuardDuty
const guardduty = new aws.guardduty.Detector("guardduty", {
enable: true,
findingPublishingFrequency: "FIFTEEN_MINUTES",
tags,
});
// SNS topic for GuardDuty findings
const securityAlertTopic = new aws.sns.Topic("security-alerts", {
name: `${projectName}-security-alerts`,
displayName: "Security Alerts",
kmsMasterKeyId: appKmsKey.id,
tags,
});
// Email subscription for security alerts
new aws.sns.TopicSubscription("security-email", {
topic: securityAlertTopic.arn,
protocol: "email",
endpoint: config.require("securityEmail"),
});
// EventBridge rule for high severity findings
const guarddutyRule = new aws.cloudwatch.EventRule("guardduty-findings", {
name: `${projectName}-guardduty-high`,
description: "Alert on high severity GuardDuty findings",
eventPattern: JSON.stringify({
source: ["aws.guardduty"],
"detail-type": ["GuardDuty Finding"],
detail: {
severity: [7, 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9],
},
}),
});
new aws.cloudwatch.EventTarget("guardduty-sns", {
rule: guarddutyRule.name,
arn: securityAlertTopic.arn,
});
// ============================================================================
// 9. SECURITY HUB
// ============================================================================
// Enable Security Hub
const securityHub = new aws.securityhub.Account("security-hub", {
enableDefaultStandards: true,
});
// Enable AWS Foundational Security Best Practices standard
new aws.securityhub.StandardsSubscription("fsbp-standard", {
standardsArn: pulumi.interpolate`arn:aws:securityhub:${aws.getRegionOutput().name}::standards/aws-foundational-security-best-practices/v/1.0.0`,
}, { dependsOn: [securityHub] });
// Enable CIS AWS Foundations Benchmark
new aws.securityhub.StandardsSubscription("cis-standard", {
standardsArn: pulumi.interpolate`arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0`,
}, { dependsOn: [securityHub] });
// ============================================================================
// 10. CLOUDWATCH ALARMS FOR SECURITY EVENTS
// ============================================================================
// Metric filter for unauthorized API calls
const unauthorizedApiFilter = new aws.cloudwatch.LogMetricFilter("unauthorized-api-calls", {
name: "UnauthorizedAPICalls",
logGroupName: cloudtrailLogGroup.name,
pattern: '{ ($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") }',
metricTransformation: {
name: "UnauthorizedAPICalls",
namespace: "CloudTrailMetrics",
value: "1",
defaultValue: "0",
},
});
new aws.cloudwatch.MetricAlarm("unauthorized-api-alarm", {
name: `${projectName}-unauthorized-api`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 1,
metricName: "UnauthorizedAPICalls",
namespace: "CloudTrailMetrics",
period: 300,
statistic: "Sum",
threshold: 5,
alarmDescription: "Alert on unauthorized API calls",
alarmActions: [securityAlertTopic.arn],
tags,
}, { dependsOn: [unauthorizedApiFilter] });
// Metric filter for root account usage
const rootAccountFilter = new aws.cloudwatch.LogMetricFilter("root-account-usage", {
name: "RootAccountUsage",
logGroupName: cloudtrailLogGroup.name,
pattern: '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }',
metricTransformation: {
name: "RootAccountUsage",
namespace: "CloudTrailMetrics",
value: "1",
defaultValue: "0",
},
});
new aws.cloudwatch.MetricAlarm("root-account-alarm", {
name: `${projectName}-root-account`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 1,
metricName: "RootAccountUsage",
namespace: "CloudTrailMetrics",
period: 60,
statistic: "Sum",
threshold: 0,
alarmDescription: "Alert on root account usage",
alarmActions: [securityAlertTopic.arn],
tags,
}, { dependsOn: [rootAccountFilter] });
// Metric filter for IAM policy changes
const iamPolicyFilter = new aws.cloudwatch.LogMetricFilter("iam-policy-changes", {
name: "IAMPolicyChanges",
logGroupName: cloudtrailLogGroup.name,
pattern: '{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}',
metricTransformation: {
name: "IAMPolicyChanges",
namespace: "CloudTrailMetrics",
value: "1",
defaultValue: "0",
},
});
new aws.cloudwatch.MetricAlarm("iam-policy-alarm", {
name: `${projectName}-iam-changes`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 1,
metricName: "IAMPolicyChanges",
namespace: "CloudTrailMetrics",
period: 300,
statistic: "Sum",
threshold: 0,
alarmDescription: "Alert on IAM policy changes",
alarmActions: [securityAlertTopic.arn],
tags,
}, { dependsOn: [iamPolicyFilter] });
// ============================================================================
// OUTPUTS
// ============================================================================
export const appKmsKeyId = appKmsKey.id;
export const appKmsKeyArn = appKmsKey.arn;
export const s3KmsKeyId = s3KmsKey.id;
export const dbSecretArn = dbSecret.arn;
export const appRoleArn = appRole.arn;
export const secureBucketName = secureBucket.bucket;
export const cloudtrailBucketName = cloudtrailBucket.bucket;
export const securityAlertTopicArn = securityAlertTopic.arn;
export const vpcId = vpc.id;
export const appSecurityGroupId = appSg.id;const secureLambda = new aws.lambda.Function("secure-function", {
runtime: aws.lambda.Runtime.Python3d11,
handler: "index.handler",
role: lambdaRole.arn,
timeout: 30,
reservedConcurrentExecutions: 10,
deadLetterConfig: {
targetArn: dlqQueue.arn,
},
environment: {
variables: {
SECRET_ARN: dbSecret.arn,
},
},
vpcConfig: {
subnetIds: privateSubnets.map(s => s.id),
securityGroupIds: [lambdaSg.id],
},
tracingConfig: {
mode: "Active",
},
kmsKeyArn: appKmsKey.arn,
code: new pulumi.asset.AssetArchive({
"index.py": new pulumi.asset.StringAsset(`
import boto3
import json
import os
secrets_client = boto3.client('secretsmanager')
def handler(event, context):
# Retrieve secrets securely
secret_arn = os.environ['SECRET_ARN']
secret = secrets_client.get_secret_value(SecretId=secret_arn)
credentials = json.loads(secret['SecretString'])
# Use credentials securely
# Never log sensitive data
return {
'statusCode': 200,
'body': json.dumps('Success')
}
`),
}),
tags,
});const secureDb = new aws.rds.Instance("secure-db", {
identifier: `${projectName}-db`,
engine: "postgres",
engineVersion: "15.5",
instanceClass: "db.t3.small",
allocatedStorage: 20,
storageEncrypted: true,
kmsKeyId: rdsKmsKey.id,
username: "dbadmin",
password: dbPassword,
dbSubnetGroupName: dbSubnetGroup.name,
vpcSecurityGroupIds: [dbSg.id],
publiclyAccessible: false,
multiAz: true,
backupRetentionPeriod: 30,
backupWindow: "03:00-04:00",
maintenanceWindow: "mon:04:00-mon:05:00",
enabledCloudwatchLogsExports: ["postgresql", "upgrade"],
performanceInsightsEnabled: true,
performanceInsightsKmsKeyId: rdsKmsKey.id,
deletionProtection: true,
copyTagsToSnapshot: true,
iamDatabaseAuthenticationEnabled: true,
tags,
});const secureTaskDefinition = new aws.ecs.TaskDefinition("secure-task", {
family: `${projectName}-task`,
networkMode: "awsvpc",
requiresCompatibilities: ["FARGATE"],
cpu: "512",
memory: "1024",
executionRoleArn: taskExecutionRole.arn,
taskRoleArn: taskRole.arn,
containerDefinitions: JSON.stringify([{
name: "app",
image: "myapp:latest",
essential: true,
readonlyRootFilesystem: true,
linuxParameters: {
capabilities: {
drop: ["ALL"],
},
},
secrets: [
{
name: "DB_PASSWORD",
valueFrom: dbSecret.arn,
},
],
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-group": logGroup.name,
"awslogs-region": aws.getRegionOutput().name,
"awslogs-stream-prefix": "app",
},
},
}]),
tags,
});# Configure security email for alerts
pulumi config set securityEmail ops@example.com
# Set database password
pulumi config set --secret dbPassword YourSecurePassword123!
# Set trusted account ID for cross-account access
pulumi config set trustedAccountId 123456789012
# Set external ID
pulumi config set --secret externalId unique-external-id-12345
# Deploy security infrastructure
pulumi up
# Verify security controls
aws cloudtrail describe-trails
aws guardduty list-detectors
aws securityhub describe-hub# Get Config compliance summary
aws configservice describe-compliance-by-config-rule
# Get Security Hub findings
aws securityhub get-findings --filters '{"SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}]}'
# Export CloudTrail logs
aws s3 sync s3://$(pulumi stack output cloudtrailBucketName) ./cloudtrail-logs/# Investigate suspicious activity
aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=suspicious-user
# Review GuardDuty findings
aws guardduty list-findings --detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
# Disable compromised access key
aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive --user-name compromised-user
# Rotate secrets
aws secretsmanager rotate-secret --secret-id $(pulumi stack output dbSecretArn)# Disable protection before cleanup
# aws rds modify-db-instance --db-instance-identifier secure-app-db --no-deletion-protection
# Destroy infrastructure
pulumi destroy
# Remove logs and backups manually if needed