昨天我們成功將 Kyo-System 容器化並部署到 Amazon ECS 上,應用現在已經在雲端穩定運行。今天我們要為系統加上全方位的安全防護和監控機制,確保我們的 OTP 服務在生產環境中安全可靠。
在雲端環境中,安全性和監控是很重要的:
我們的安全架構採用縱深防禦策略:
🌐 Internet
↓
🛡️ WAF (Web Application Firewall)
↓
⚖️ ALB (Application Load Balancer)
↓
🏢 VPC Private Subnets
↓
🐳 ECS Tasks
↓
🔐 Secrets Manager
首先建立 WAF 來防護我們的應用:
// infrastructure/lib/kyo-security-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
interface SecurityStackProps extends cdk.StackProps {
loadBalancer: elbv2.ApplicationLoadBalancer;
}
export class KyoSecurityStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: SecurityStackProps) {
super(scope, id, props);
// 建立 WAF Log Group
const wafLogGroup = new logs.LogGroup(this, 'WAFLogGroup', {
logGroupName: '/aws/wafv2/kyo-waf',
retention: logs.RetentionDays.ONE_MONTH,
});
// 建立 WAF Web ACL
const webAcl = new wafv2.CfnWebACL(this, 'KyoWebACL', {
scope: 'REGIONAL',
defaultAction: { allow: {} },
description: 'Kyo OTP Service WAF Rules',
rules: [
// 1. AWS 管理規則:一般安全防護
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
overrideAction: { none: {} },
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
excludedRules: [
// 排除可能誤擋的規則
{ name: 'SizeRestrictions_BODY' },
{ name: 'GenericRFI_BODY' },
],
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'CommonRuleSetMetric',
sampledRequestsEnabled: true,
},
},
// 2. 已知惡意輸入防護
{
name: 'AWSManagedRulesKnownBadInputsRuleSet',
priority: 2,
overrideAction: { none: {} },
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesKnownBadInputsRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'KnownBadInputsMetric',
sampledRequestsEnabled: true,
},
},
// 3. Linux 特定攻擊防護
{
name: 'AWSManagedRulesLinuxRuleSet',
priority: 3,
overrideAction: { none: {} },
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesLinuxRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'LinuxRuleSetMetric',
sampledRequestsEnabled: true,
},
},
// 4. 速率限制:每個 IP 每 5 分鐘最多 1000 個請求
{
name: 'RateLimitRule',
priority: 4,
action: { block: {} },
statement: {
rateBasedStatement: {
limit: 1000,
aggregateKeyType: 'IP',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'RateLimitMetric',
sampledRequestsEnabled: true,
},
},
// 5. 地理位置限制(可選)
{
name: 'GeoBlockRule',
priority: 5,
action: { block: {} },
statement: {
geoMatchStatement: {
countryCodes: ['CN', 'RU', 'KP'], // 依需求調整
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'GeoBlockMetric',
sampledRequestsEnabled: true,
},
},
// 6. 自訂規則:保護 API 端點
{
name: 'ProtectAPIEndpoints',
priority: 6,
action: { block: {} },
statement: {
andStatement: {
statements: [
{
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
positionalConstraint: 'STARTS_WITH',
searchString: '/api/',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
{
notStatement: {
statement: {
orStatement: {
statements: [
// 允許的方法
{
byteMatchStatement: {
fieldToMatch: { method: {} },
positionalConstraint: 'EXACTLY',
searchString: 'GET',
textTransformations: [
{ priority: 0, type: 'NONE' },
],
},
},
{
byteMatchStatement: {
fieldToMatch: { method: {} },
positionalConstraint: 'EXACTLY',
searchString: 'POST',
textTransformations: [
{ priority: 0, type: 'NONE' },
],
},
},
],
},
},
},
},
],
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'APIProtectionMetric',
sampledRequestsEnabled: true,
},
},
],
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'KyoWebACLMetric',
sampledRequestsEnabled: true,
},
});
// 將 WAF 關聯到 ALB
new wafv2.CfnWebACLAssociation(this, 'WebACLAssociation', {
resourceArn: props.loadBalancer.loadBalancerArn,
webAclArn: webAcl.attrArn,
});
// 輸出 WAF ARN
new cdk.CfnOutput(this, 'WebACLArn', {
value: webAcl.attrArn,
description: 'WAF Web ACL ARN',
});
}
}
將敏感資料遷移到 AWS Secrets Manager:
// infrastructure/lib/kyo-secrets-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class KyoSecretsStack extends cdk.Stack {
public readonly secret: secretsmanager.Secret;
public readonly secretAccessRole: iam.Role;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 建立 Secrets Manager
this.secret = new secretsmanager.Secret(this, 'KyoSecrets', {
secretName: 'kyo-secrets',
description: 'Kyo OTP Service secrets',
generateSecretString: {
secretStringTemplate: JSON.stringify({
MITAKE_USERNAME: '',
MITAKE_PASSWORD: '',
JWT_SECRET: '',
REDIS_PASSWORD: '',
}),
generateStringKey: 'AUTO_GENERATED_PASSWORD',
excludeCharacters: '"@/\\',
},
});
// 建立 IAM 角色供 ECS 任務使用
this.secretAccessRole = new iam.Role(this, 'SecretAccessRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
description: 'Role for ECS tasks to access secrets',
});
// 授權讀取 secrets
this.secret.grantRead(this.secretAccessRole);
// 輸出 secret ARN
new cdk.CfnOutput(this, 'SecretArn', {
value: this.secret.secretArn,
description: 'Secrets Manager ARN',
});
}
}
// 更新 infrastructure/lib/kyo-infrastructure-stack.ts
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// 在 VPC 建立後,加強安全群組設定
const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
vpc: this.vpc,
description: 'Security group for Application Load Balancer',
allowAllOutbound: false,
});
// ALB 只允許 HTTP/HTTPS 入站
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic'
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic'
);
// ALB 出站只能連接到 ECS
albSecurityGroup.addEgressRule(
ec2.Peer.securityGroupId(ecsSecurityGroup.securityGroupId),
ec2.Port.tcp(3000),
'Allow traffic to ECS tasks'
);
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'ECSSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ECS tasks',
allowAllOutbound: false,
});
// ECS 只接受來自 ALB 的流量
ecsSecurityGroup.addIngressRule(
ec2.Peer.securityGroupId(albSecurityGroup.securityGroupId),
ec2.Port.tcp(3000),
'Allow traffic from ALB'
);
// ECS 出站到資料庫和 Redis
ecsSecurityGroup.addEgressRule(
ec2.Peer.securityGroupId(rdsSecurityGroup.securityGroupId),
ec2.Port.tcp(5432),
'Allow traffic to RDS'
);
ecsSecurityGroup.addEgressRule(
ec2.Peer.securityGroupId(redisSecurityGroup.securityGroupId),
ec2.Port.tcp(6379),
'Allow traffic to Redis'
);
// 允許 HTTPS 出站(用於 Secrets Manager 和其他 AWS 服務)
ecsSecurityGroup.addEgressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS outbound'
);
// infrastructure/lib/kyo-audit-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { Construct } from 'constructs';
interface AuditStackProps extends cdk.StackProps {
alertEmail: string;
}
export class KyoAuditStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: AuditStackProps) {
super(scope, id, props);
// 建立 S3 bucket 存儲 CloudTrail 日誌
const cloudTrailBucket = new s3.Bucket(this, 'CloudTrailBucket', {
bucketName: `kyo-cloudtrail-${this.account}-${this.region}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
lifecycleRules: [
{
id: 'DeleteOldLogs',
enabled: true,
expiration: cdk.Duration.days(90),
noncurrentVersionExpiration: cdk.Duration.days(30),
},
],
});
// 建立 SNS 主題用於告警
const alertTopic = new sns.Topic(this, 'SecurityAlertTopic', {
topicName: 'kyo-security-alerts',
displayName: 'Kyo Security Alerts',
});
// 訂閱告警郵件
alertTopic.addSubscription(
new subscriptions.EmailSubscription(props.alertEmail)
);
// 建立 CloudTrail
const trail = new cloudtrail.Trail(this, 'KyoAuditTrail', {
trailName: 'kyo-audit-trail',
bucket: cloudTrailBucket,
includeGlobalServiceEvents: true,
isMultiRegionTrail: true,
enableFileValidation: true,
// 記錄管理事件和資料事件
managementEvents: cloudtrail.ReadWriteType.ALL,
// 監控關鍵資源的資料事件
s3BucketEvents: [
{
bucket: cloudTrailBucket,
events: cloudtrail.ReadWriteType.ALL,
includeManagementEvents: true,
},
],
});
// CloudWatch 日誌整合
trail.logAllLambdaDataEvents();
trail.logAllS3DataEvents();
// 建立 CloudWatch Alarms
this.createSecurityAlarms(alertTopic);
// 輸出重要資訊
new cdk.CfnOutput(this, 'CloudTrailArn', {
value: trail.trailArn,
description: 'CloudTrail ARN',
});
new cdk.CfnOutput(this, 'AlertTopicArn', {
value: alertTopic.topicArn,
description: 'Security Alert Topic ARN',
});
}
private createSecurityAlarms(alertTopic: sns.Topic) {
// 這裡可以加入各種安全告警
// 例如:失敗的登入嘗試、權限變更等
}
}
// 在 ECS 任務定義中加入安全監控
const backendTaskDefinition = new ecs.FargateTaskDefinition(this, 'BackendTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
taskRole: secretsStack.secretAccessRole, // 使用專用角色
});
const backendContainer = backendTaskDefinition.addContainer('backend', {
image: ecs.ContainerImage.fromEcrRepository(backendRepo, 'latest'),
environment: {
NODE_ENV: 'production',
PORT: '3000',
DATABASE_URL: `postgresql://kyouser:kyopass@${props.rdsEndpoint}:5432/kyodb`,
REDIS_URL: `redis://${props.redisEndpoint}:6379`,
// 安全設定
HELMET_ENABLED: 'true',
CORS_ORIGIN: `https://${props.domainName}`,
RATE_LIMIT_ENABLED: 'true',
LOG_LEVEL: 'info',
},
secrets: {
// 從 Secrets Manager 讀取敏感資料
MITAKE_USERNAME: ecs.Secret.fromSecretsManager(
secretsStack.secret,
'MITAKE_USERNAME'
),
MITAKE_PASSWORD: ecs.Secret.fromSecretsManager(
secretsStack.secret,
'MITAKE_PASSWORD'
),
JWT_SECRET: ecs.Secret.fromSecretsManager(
secretsStack.secret,
'JWT_SECRET'
),
},
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'backend',
logGroup: new logs.LogGroup(this, 'BackendLogGroup', {
logGroupName: '/aws/ecs/kyo-backend',
retention: logs.RetentionDays.TWO_WEEKS,
}),
}),
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost:3000/api/health || exit 1'],
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
retries: 3,
startPeriod: cdk.Duration.seconds(60),
},
});
更新後端應用的安全設定:
// apps/kyo-otp-service/src/plugins/security.ts
import fp from 'fastify-plugin';
import helmet from '@fastify/helmet';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
export default fp(async function (fastify) {
// Helmet 安全頭
await fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false, // 為了相容性
});
// CORS 設定
await fastify.register(cors, {
origin: (origin, callback) => {
const allowedOrigins = [
process.env.CORS_ORIGIN,
'http://localhost:5174', // 開發環境
].filter(Boolean);
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'), false);
}
},
credentials: true,
});
// 速率限制
await fastify.register(rateLimit, {
max: 100, // 每個 IP 每分鐘最多 100 個請求
timeWindow: '1 minute',
errorResponseBuilder: (request, context) => {
return {
code: 'E_RATE_LIMIT',
message: 'Too many requests',
statusCode: 429,
issues: {
resetInSec: Math.ceil(context.ttl / 1000),
},
};
},
});
// API 特定速率限制
await fastify.register(rateLimit, {
max: 5, // OTP 發送每個 IP 每小時最多 5 次
timeWindow: '1 hour',
keyGenerator: (request) => `otp-send:${request.ip}`,
continueExceeding: true,
skipOnError: false,
}, { prefix: '/api/otp/send' });
});
// infrastructure/lib/kyo-monitoring-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { Construct } from 'constructs';
interface MonitoringStackProps extends cdk.StackProps {
loadBalancer: elbv2.ApplicationLoadBalancer;
clusterName: string;
serviceName: string;
alertEmail: string;
}
export class KyoMonitoringStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MonitoringStackProps) {
super(scope, id, props);
// 建立告警主題
const alertTopic = new sns.Topic(this, 'MonitoringAlerts', {
topicName: 'kyo-monitoring-alerts',
});
alertTopic.addSubscription(
new subscriptions.EmailSubscription(props.alertEmail)
);
// 應用程式健康告警
const healthAlarm = new cloudwatch.Alarm(this, 'HealthCheckAlarm', {
alarmName: 'kyo-health-check-failed',
alarmDescription: 'ALB health check is failing',
metric: props.loadBalancer.metricUnhealthyHostCount(),
threshold: 1,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.BREACHING,
});
healthAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// 高錯誤率告警
const errorRateAlarm = new cloudwatch.Alarm(this, 'HighErrorRateAlarm', {
alarmName: 'kyo-high-error-rate',
alarmDescription: 'High error rate detected',
metric: new cloudwatch.MathExpression({
expression: '(m1/m2)*100',
usingMetrics: {
m1: props.loadBalancer.metricHttpCodeTarget(
elbv2.HttpCodeTarget.TARGET_5XX_COUNT
),
m2: props.loadBalancer.metricRequestCount(),
},
label: 'Error Rate %',
}),
threshold: 5, // 5% 錯誤率
evaluationPeriods: 3,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
errorRateAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// 高延遲告警
const latencyAlarm = new cloudwatch.Alarm(this, 'HighLatencyAlarm', {
alarmName: 'kyo-high-latency',
alarmDescription: 'High response latency detected',
metric: props.loadBalancer.metricTargetResponseTime(),
threshold: 2, // 2 秒
evaluationPeriods: 2,
});
latencyAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// CPU 使用率告警
const cpuAlarm = new cloudwatch.Alarm(this, 'HighCPUAlarm', {
alarmName: 'kyo-high-cpu',
alarmDescription: 'High CPU utilization detected',
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'CPUUtilization',
dimensionsMap: {
ClusterName: props.clusterName,
ServiceName: props.serviceName,
},
statistic: 'Average',
}),
threshold: 80, // 80% CPU 使用率
evaluationPeriods: 2,
});
cpuAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// 記憶體使用率告警
const memoryAlarm = new cloudwatch.Alarm(this, 'HighMemoryAlarm', {
alarmName: 'kyo-high-memory',
alarmDescription: 'High memory utilization detected',
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'MemoryUtilization',
dimensionsMap: {
ClusterName: props.clusterName,
ServiceName: props.serviceName,
},
statistic: 'Average',
}),
threshold: 85, // 85% 記憶體使用率
evaluationPeriods: 2,
});
memoryAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// WAF 阻擋請求告警
const wafBlockAlarm = new cloudwatch.Alarm(this, 'WAFBlockAlarm', {
alarmName: 'kyo-waf-blocks',
alarmDescription: 'High number of blocked requests by WAF',
metric: new cloudwatch.Metric({
namespace: 'AWS/WAFV2',
metricName: 'BlockedRequests',
dimensionsMap: {
WebACL: 'KyoWebACL',
Region: this.region,
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
threshold: 100, // 5 分鐘內超過 100 個被阻擋的請求
evaluationPeriods: 1,
});
wafBlockAlarm.addAlarmAction(
new cloudwatch.SnsAction(alertTopic)
);
// 建立 Dashboard
const dashboard = new cloudwatch.Dashboard(this, 'KyoDashboard', {
dashboardName: 'kyo-otp-service-dashboard',
});
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Request Count',
left: [props.loadBalancer.metricRequestCount()],
width: 12,
height: 6,
}),
new cloudwatch.GraphWidget({
title: 'Response Time',
left: [props.loadBalancer.metricTargetResponseTime()],
width: 12,
height: 6,
}),
new cloudwatch.GraphWidget({
title: 'Error Rate',
left: [
props.loadBalancer.metricHttpCodeTarget(elbv2.HttpCodeTarget.TARGET_4XX_COUNT),
props.loadBalancer.metricHttpCodeTarget(elbv2.HttpCodeTarget.TARGET_5XX_COUNT),
],
width: 12,
height: 6,
}),
);
// 輸出
new cdk.CfnOutput(this, 'DashboardURL', {
value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=kyo-otp-service-dashboard`,
description: 'CloudWatch Dashboard URL',
});
}
}
#!/bin/bash
# scripts/deploy-secure.sh
set -e
echo "🔒 Deploying secure infrastructure..."
# 部署基礎設施
cdk deploy KyoInfrastructureStack --require-approval never
# 部署安全堆疊
cdk deploy KyoSecretsStack --require-approval never
cdk deploy KyoSecurityStack --require-approval never
cdk deploy KyoAuditStack --require-approval never
# 部署 ECS 堆疊
cdk deploy KyoEcsStack --require-approval never
# 部署監控堆疊
cdk deploy KyoMonitoringStack --require-approval never
echo "✅ Secure deployment completed!"
# 建構並推送容器映像
./scripts/deploy.sh
echo "🎉 All deployments completed successfully!"
我們建立了企業級的安全防護體系:
✅ 多層防護:WAF + 安全群組 + 應用層防護
✅ 敏感資料保護:Secrets Manager 集中管理
✅ 全面監控:CloudWatch + CloudTrail 完整可見性
✅ 即時告警:多維度監控指標和告警機制
✅ 合規稽核:完整的操作記錄和安全日誌
✅ 自動化部署:一鍵部署安全基礎設施