AWS CDKでAWS WAFのルールを作る試み
はじめに
いろいろ書いてますが、githubのコードかこのあたりを見てもらえればOKです。
AWS CDKにおけるAWS WAF
現在、AWS CDKにはAWS WAFのL2 Constructがないため、以下のような書き方になります。
これはAWSマネージドルールグループ1つと自分で定義したルールをWebACLに追加したものです。
export class CdktempStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const webAcl = new waf.CfnWebACL(this, "WebAcl", {
defaultAction: { block: {} },
name: "WebAclName",
scope: "REGIONAL",
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "WebAcl",
},
rules: [
{
priority: 1,
name: "managedRule",
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: "managedRule",
},
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesCommonRuleSet",
},
},
overrideAction: { none: {} }
},
{
priority: 10,
name: "myRule",
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: "myRule",
},
statement: {
notStatement: {
statement: {
geoMatchStatement: {
countryCodes: ["JP"],
},
},
},
},
},
],
});
}
}
L1コンストラクタしかないため、という理由が大きいかもしれないですが、やりたいことに対して、ほぼAWS WAFのJSONと同じようなものを書かないといけないのはなかなか手間です。
ないなら作ろうの精神でAWS CDKでAWS WAFを扱うやつを作った!というのが今回の趣旨です。
こう書きたい
このrulesの書き方にある程度習熟しているという前提は必要ですが、単純にルールを有効にするだけの場合、上記のサンプルで見せたAWSマネージドルールグループの部分(managedRule
)のようにするのはそんなに難しくはないはずです。
一方で、ORやAND,NOTを駆使して、複数のステートメントで条件にするととたんにめんどくさくなります。上記のサンプルではmyRule
の部分です。
statementを何回書かせる気なんだ
statement: {
notStatement: {
statement: {
geoMatchStatement: {
countryCodes: ["JP"],
},
},
},
}
完全に個人の意見ですが、例えばルール部を以下のように書いてあるとよさそうに見えます。Cloudflareとかに影響受けています。
block( not( geoip.country = ["JP"] ) )
aws-cdk-examplesのwaf
aws-cdk-examplesにもwafを扱ったサンプルがあります。
これをベースに拡張していたのが最初の一歩でした。
AWSマネージドルールグループについては以下の書き方と同じようにしています。
ちなみにサンプルではexcludedRules
が扱えるような書き方ですが実装されてませんでした。
一方で、国コードのルールやRateLimitのルールなどもありますが、この辺りを大きく変えています。
先ほど記載したANDやORが書けるようにし、visibilityConfig
などを省略できるようになればいい感じです。
作りたいもの
作りたいもの
と書いていますが、実際にはルールを作りながら必要なものを実装しています。
その時欲しくなかったから実装されていない、みたいなのも多くあります。
AWSマネージドルールグループに対してほしい機能は以下です。
excludedRules
とscopeDownStatement
を使えるようにすることで、マネージドルールグループに細かい調整ができるようになります。
- 特定ルールグループを有効にする
-
excludedRules
を扱う -
scopeDownStatement
を扱う
自分で作るカスタムルールに対してほしい機能は以下です。
- ANDやOR,NOTを扱いやすくする
- IPセットやGeoベース、その他のルールを関数で作るようする。(AND,OR,NOTを駆使して自分で実装しやすくする)
また共通してプライオリティ(ルールの優先順位)を自動で付与できるようにしています。
作ったもの
こう書きたいものは
block( not( geoip.country = ["JP"] ) )
こうなった
WafStatement.block(
"name",
priority,
WafStatement.not(
WafStatement.matchCountryCodes(["JP"})
)
)
サンプルの方針
今回のサンプルでは以下のファイルに分かれています。
どうわければいいのかは結構迷走してましたし、今も迷ってます。
- IPアドレスやルール構成などの変更する場所
- /lib/waf-config.ts
- IPセットやRateルールに利用する各種値
- /lib/waf.ts
- 具体的なルールを設定する場所
-
CfnWebACL.RuleProperty
を返す関数として実装する
-
- 自分が欲しいルールを
rules
に追加する
- 具体的なルールを設定する場所
- /lib/waf-config.ts
- ルールステートメントなど変更しない場所
- /lib/utils/waf/webacl.ts
- WebACLのScope
- (作ったけどいらなかった)
- /lib/utils/waf/statements.ts
- ルールのパーツを定義
- 今回の主役
- /lib/utils/waf/ipsets.ts
- IPSetsを扱う
- ipv4とipv6を同時に扱う
- /lib/utils/waf/webacl.ts
cdk-waf-sample
以下に作成したものを置いています。
いくつかファイルに分かれているのでStackから順に説明していきます。
Stack
waf.ts
のWaf
コンストラクタを呼び出しています。
どのルールを使うかなどは、StackではなくWaf
コンストラクタで書くようにしています。
Wafコンストラクタ
rules
にmakeRules
関数で作ったルールのリストを入れています。
makeRuels
の中で具体的なルールを作っていきます。
AWSマネージドルールグループ
createManagedRules
関数内でmanagedRules
をWafStatements.managedRuleGroup()関数で処理しています。
AWSマネージドルールグループ名やプライオリティを入力し、CfnWebACL.RuleProperty
を返します。
static managedRuleGroup(
r: listOfRules,
startPriorityNumber: number,
index: number,
): CfnRuleGroup.RuleProperty {
var stateProp: CfnWebACL.StatementProperty = {
managedRuleGroupStatement: {
name: r.name,
vendorName: 'AWS',
excludedRules: r.excludedRules.map((ruleName) => ({
name: ruleName,
})),
scopeDownStatement: r.scopeDownStatement,
},
};
var overrideAction: CfnWebACL.OverrideActionProperty = { none: {} };
var rule: CfnWebACL.RuleProperty = {
name: r.name,
priority:
r.priority !== undefined ? r.priority : startPriorityNumber + index,
overrideAction: overrideAction,
statement: stateProp,
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: r.name,
},
};
return rule;
}
カスタムルール
今回のサンプルの中で複数の条件を使っているcreateSizeRestrictionExcludedAdminIpsを例にあげて説明します。
createSizeRestrictionExcludedAdminIps
は以下のようなルールです。
- 条件にマッチしたリクエストをブロック
- 以下の両方にマッチする
- 16KB以上のBodyのサイズを持つ
- 以下の条件に両方にマッチしない
- 特定パス
-
/api/
で始まる -
/setup
に完全一致する
-
- 管理者のIPアドレス
- 管理者のIPアドレスを定義していない場合には特定パスだけで判断
- 特定パス
- 以下の両方にマッチする
つまり、管理者には一部パスに16KB以上のリクエストを許可しているようなルールです。
private createSizeRestrictionExcludedAdminIps(
priority: number,
adminIpsSetList: CfnIPSet[]
): CfnRuleGroup.RuleProperty {
const urlConditons = WafStatements.or(
WafStatements.startsWithURL("/api/"),
WafStatements.exactlyURL("/setup")
);
let combinedConditions;
if (adminIpsSetList.length === 0) {
combinedConditions = urlConditons;
} else {
combinedConditions = WafStatements.and(
urlConditons,
WafStatements.ipv4v6Match(adminIpsSetList)
);
}
return WafStatements.block(
"SizeRestriction",
priority,
WafStatements.and(
WafStatements.oversizedRequestBody(16 * 1024), //16KB
WafStatements.not(combinedConditions)
)
);
}
おわりに
AWS WAFをCDKで書く試みでした。
WafStatements
は自分が欲しかった部分しか実装していないので、実際に使うと足りない箇所はあるかもしれないが、その都度実装すればいいかな、と
もとのL1 ConstructやAWS WAFで利用できるJSONフォーマットは書きづらい一方で、AWS WAFに詳しくなくてもルールがどういった動きをするのかが読めると考えています。
今回のCDKのサンプルは若干の書きやすさを得る代わりに、部分的に複雑になったために読みづらい部分があるかもなぁ、というのが反省点です。設定する場所が2か所になって嬉しいんだっけ?というところとか。
特にIPセット周りのIPセットが定義されていなかったら…
や、IPv4のみ定義された場合には…
といった場合の対応が手間がかかった部分ですが、CloudFormationの場合にはもっと時間を消費すると思っているので、よかったということにしておきます。
このサンプルの元となったものが個人でちゃんと書いた初めてのTypeScriptなので、いろいろ多めに見てください。(というか書き方こうしたほうがいい、とかあったら教えてほしい)
Discussion