はじめに
本記事では、AWS WAFをALBに設定し、
XSSやSQLインジェクションなどの攻撃をアプリに届く前に遮断できる構成を作成します。
これにより、アプリではなく入口(ALB)でリクエストを検査し、
一般的なWeb攻撃を自動でブロックできるようになります。
本シリーズの構成
1. VPC編
2. Route53 独自ドメイン編
3. ACM HTTPS化編
4.[WAFセキュリティ編(本記事)
5. ECS Auto Scaling編
6. CI/CD編
WAF適用後の構成
- 図は簡略化しており、実際の構成では複数AZにまたがって配置されます。
- WAFはALBに設定され、ALBに届いたリクエストを検査します。
前提条件
- これまでの記事の手順を実施し、独自ドメインでALBにHTTPSアクセスできる状態になっている。
4-1. ALBを外部から参照できるようにする
WAFでALBを紐づけるため、albをスタックのプロパティとして公開します。
対象ファイル
lib/alb-stack.ts
export class AlbStack extends cdk.Stack {
public readonly alb: elbv2.ApplicationLoadBalancer;
public readonly targetGroup: elbv2.ApplicationTargetGroup;
public readonly albSecurityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, props: AlbStackProps) {
super(scope, id, props);
//...
this.alb = new elbv2.ApplicationLoadBalancer(this, 'NextjsAlb', {
vpc: props.vpc,
internetFacing: true,
securityGroup: this.albSecurityGroup,
});
// 以降も alb を this.alb とする
}
}
ここでは、ALBを外部から参照できるようにするため、
albをクラスのプロパティとして公開します。
4-2. WafStackを作成する
WAF本体(Web ACL)を作成し、ALBと関連付けます。
利用するAWS Managed Rules:
- AWSManagedRulesCommonRuleSet(XSS / SQLインジェクション / コマンドインジェクション 等)
- AWSManagedRulesKnownBadInputsRuleSet(明らかに不正な入力)
新規作成ファイル
lib/waf-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
interface WafStackProps extends cdk.StackProps {
alb: elbv2.IApplicationLoadBalancer;
}
export class WafStack extends cdk.Stack {
public readonly webAcl: wafv2.CfnWebACL;
constructor(scope: Construct, id: string, props: WafStackProps) {
super(scope, id, props);
this.webAcl = new wafv2.CfnWebACL(this, 'AppWebAcl', {
name: 'app-web-acl',
scope: 'REGIONAL',
defaultAction: {
allow: {},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AppWebAcl',
sampledRequestsEnabled: true,
},
rules: [
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
overrideAction: {
none: {},
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesCommonRuleSetMetric',
sampledRequestsEnabled: true,
},
},
{
name: 'AWSManagedRulesKnownBadInputsRuleSet',
priority: 2,
overrideAction: {
none: {},
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesKnownBadInputsRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesKnownBadInputsMetric',
sampledRequestsEnabled: true,
},
},
],
});
new wafv2.CfnWebACLAssociation(this, 'AlbWebAclAssociation', {
resourceArn: props.alb.loadBalancerArn,
webAclArn: this.webAcl.attrArn,
});
}
}
このコードのポイントは次の通りです。
-
scope: 'REGIONAL'
ALB に適用するため、REGIONALを使います。 -
defaultAction: allow
通常リクエストは許可し、ルールに一致したものだけをブロックします。 -
CfnWebACLAssociation
作成したWeb ACLをALBに関連付けます。
WAFは調整が不十分な場合、正常なリクエストをブロックしてしまうことがあります。
まずは検証環境で動作確認を行い、必要に応じて調整したうえで本番へ適用することが推奨されます。
4-3. ALBをWafStackに渡す
CDKのエントリーポイントでAlbStackのALBをWafStackに渡します。
対象ファイル
bin/sample-infra.ts
※ 本シリーズでは CDKプロジェクト名を sample-infra としています。
import { WafStack } from '../lib/waf-stack';
// 既存のスタック定義は省略
new WafStack(app, 'NextjsInfraWafStack', {
env,
alb: albStack.alb,
});
実際の関連付けは、WafStack内のCfnWebACLAssociationで行われます。
4-4. デプロイ
cdk deploy NextjsInfraWafStack --profile <プロファイル名>
本記事で作成したスタックに含まれるAWSリソースは、削除するまで料金が発生します。
検証が不要になった場合は、以下のコマンドでスタックを削除してください。
cdk destroy <スタック名> --profile <プロファイル名>
4-5. 確認
AWS WAF の確認
1.AWSコンソール → WAF と Shield → AWS WAF を開く
2.「保護パック(ウェブ ACL)」をクリック
3.作成したWeb ACLをクリック
以下を確認します。
- ルールに
AWSManagedRulesCommonRuleSetとAWSManagedRulesKnownBadInputsRuleSetが含まれている - リソースに ALB が関連付けられている
アプリケーションの動作確認
ブラウザで以下にアクセスします。
https://app.example.com
正常に表示されれば、通常リクエストは許可されています。
通常アクセスできれば、基本的な設定は完了です。
最終コード(今回追加・修正したファイル)
lib/alb-stack.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
interface AlbStackProps extends cdk.StackProps {
vpc: ec2.IVpc;
hostedZone: route53.IHostedZone;
certificate: acm.ICertificate;
}
export class AlbStack extends cdk.Stack {
public readonly alb: elbv2.ApplicationLoadBalancer;
public readonly targetGroup: elbv2.ApplicationTargetGroup;
public readonly albSecurityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, props: AlbStackProps) {
super(scope, id, props);
this.albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: true,
description: 'Security group for public ALB',
});
this.albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP from Internet'
);
this.albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS from Internet'
);
this.alb = new elbv2.ApplicationLoadBalancer(this, 'NextjsAlb', {
vpc: props.vpc,
internetFacing: true,
securityGroup: this.albSecurityGroup,
});
this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'NextjsTargetGroup', {
vpc: props.vpc,
port: 3000,
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
path: '/',
healthyHttpCodes: '200',
},
});
this.alb.addListener('HttpListener', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
defaultAction: elbv2.ListenerAction.redirect({
protocol: 'HTTPS',
port: '443',
permanent: true,
}),
});
this.alb.addListener('HttpsListener', {
port: 443,
protocol: elbv2.ApplicationProtocol.HTTPS,
certificates: [props.certificate],
defaultTargetGroups: [this.targetGroup],
});
new route53.ARecord(this, 'AppDomainRecord', {
zone: props.hostedZone,
recordName: 'app',
target: route53.RecordTarget.fromAlias(
new targets.LoadBalancerTarget(this.alb)
),
});
new cdk.CfnOutput(this, 'AlbDnsName', {
value: this.alb.loadBalancerDnsName,
});
}
}
lib/waf-stack.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
interface WafStackProps extends cdk.StackProps {
alb: elbv2.IApplicationLoadBalancer;
}
export class WafStack extends cdk.Stack {
public readonly webAcl: wafv2.CfnWebACL;
constructor(scope: Construct, id: string, props: WafStackProps) {
super(scope, id, props);
this.webAcl = new wafv2.CfnWebACL(this, 'AppWebAcl', {
name: 'app-web-acl',
scope: 'REGIONAL',
defaultAction: {
allow: {},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AppWebAcl',
sampledRequestsEnabled: true,
},
rules: [
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
overrideAction: {
none: {},
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesCommonRuleSetMetric',
sampledRequestsEnabled: true,
},
},
{
name: 'AWSManagedRulesKnownBadInputsRuleSet',
priority: 2,
overrideAction: {
none: {},
},
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesKnownBadInputsRuleSet',
},
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesKnownBadInputsMetric',
sampledRequestsEnabled: true,
},
},
],
});
new wafv2.CfnWebACLAssociation(this, 'AlbWebAclAssociation', {
resourceArn: props.alb.loadBalancerArn,
webAclArn: this.webAcl.attrArn,
});
}
}
bin/cdk-nextjs-infra.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { EcrStack } from '../lib/ecr-stack';
import { NetworkStack } from '../lib/network-stack';
import { EcsStack } from '../lib/ecs-stack';
import { CertificateStack } from '../lib/certificate-stack';
import { AlbStack } from '../lib/alb-stack';
import { ServiceStack } from '../lib/service-stack';
import { WafStack } from '../lib/waf-stack';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
new EcrStack(app, 'NextjsEcrStack', {
env,
});
const networkStack = new NetworkStack(app, 'NextjsInfraNetworkStack', {
env,
});
const ecsStack = new EcsStack(app, 'NextjsInfraEcsStack', {
env,
vpc: networkStack.vpc,
});
const hostedZone = route53.HostedZone.fromLookup(networkStack, 'HostedZone', {
domainName: 'example.com',
});
const certificateStack = new CertificateStack(app, 'NextjsInfraCertificateStack', {
env,
hostedZone,
});
const albStack = new AlbStack(app, 'NextjsInfraAlbStack', {
env,
vpc: networkStack.vpc,
hostedZone,
certificate: certificateStack.certificate,
});
new ServiceStack(app, 'NextjsInfraServiceStack', {
env,
cluster: ecsStack.cluster,
taskDefinition: ecsStack.taskDefinition,
targetGroup: albStack.targetGroup,
vpc: networkStack.vpc,
albSecurityGroup: albStack.albSecurityGroup,
});
new WafStack(app, 'NextjsInfraWafStack', {
env,
alb: albStack.alb,
});
ここまでの成果
ALBにAWS WAFを適用し、不正なリクエストを検知・ブロックできるようになりました。
次回
次回はECS Auto Scalingを設定し、負荷に応じてタスク数を自動調整する構成を作成します。