CDK v2.154.0でALBのmTLS対応がL2 Constructに対応しました(わしが作った)
5/ ALBで接続ログをS3バケットに出力可能に。Thank you @skrirdev !
— Kenji Kono (@konokenj) 2024年8月23日
6/ ALBでmTLSをサポート。TrustStoreを宣言してListenerに追加してください。Thank you @mazyu36 !
7/ S3イベント通知にSQS/SNS/Lambdaを設定したときのテストイベントを抑止するプロパティを追加
Pull Requestはこちらです。 github.com
特定のクライアント証明書を持つクライアントのみ接続できるやつですね。昨日の詳しい内容は安定のクラメソさんの記事が参考になります。
のんピ(non____97)さんの記事はコントリビュート時にも参考にさせていただきました(感謝)。
ざっとL2 Constructの使い方をまとめるとともに、いくつかコントリビュート時に詰まった点があるのでまとめておきます。
目次
L2 Constructの使い方
ALBのmTLSには「トラストストアモード(ALBがクライアント証明書を検証)」と「パススルーモード(ALBの背後のバックエンドサーバーがクライアント証明書を検証)」の2つがありますが、おそらく使われることがより多い(と思う)前者を扱います。
コントリビュート時のinteg testで実際に使っているので、手っ取り早く実装を知りたい場合はこれをみていただくのが良いかと思います。
なお前提として、クライアント証明書など必要なものは事前準備済みとします。この辺りは上記integ testの冒頭にもコマンドを用意してあります。
mTLSで登場する証明書の種類を詳しく知りたい方は以下の記事が参考になります。
以下ざっと流れを説明します(説明のしやすさなどを踏まえて、integ testのリソース定義と順序を一部入れ替えています)
1. VPC、ホストゾーン、ACM、ALBの用意
この辺りは通常通りなのでざっと流します。
// VPC作成 const vpc = new ec2.Vpc(this, 'Stack'); // ホストゾーン作成。IDやゾーン名はpropsで渡す。 const hostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, 'HostedZone', { hostedZoneId: props.hostedZoneId, zoneName: props.hostedZoneName, }); // ACMで証明書生成。ドメイン名はpropsで渡す。 const certificate = new acm.Certificate(this, 'Certificate', { domainName: props.domainName, validation: acm.CertificateValidation.fromDns(hostedZone), }); // ALB生成 const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true, }); // ALBをターゲットとするAレコードを生成 new route53.ARecord(this, 'ARecord', { target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(lb)), zone: hostedZone, });
2. CA証明書をS3バケットに配置し、トラストストアを作成
メインの箇所1つ目です。S3バケットにあらかじめ用意したCA証明書を配置し、それを参照する形でトラストストアを作成します。
以下の例ではBucketDeploymentを使用して、cdk deploy
の過程でアップロードしています。
この場合デプロイとトラストストアの依存関係を定義しておかないと、CA証明書の格納が終わる前にトラストストアの作成が走る→CA証明書が見つからない、となり失敗してしまう点に注意が必要です。
// バケットを作成 const bucket = new s3.Bucket(this, 'Bucket', { autoDeleteObjects: true, removalPolicy: RemovalPolicy.DESTROY, }); // バケットにCA証明書を配置 const deploy = new s3deploy.BucketDeployment(this, 'DeployCaCert', { sources: [s3deploy.Source.asset(path.join(__dirname, 'mtls'))], destinationBucket: bucket, }); // トラストストアを作成 const trustStore = new elbv2.TrustStore(this, 'Store', { bucket, key: 'rootCA_cert.pem', }); // S3へのCA証明書配置が終わってからトラストストアを作成するよう指定 trustStore.node.addDependency(deploy);
3. トラストストアをALBのリスナーに紐付け
リスナーのPropertyのmutualAuthentication
においてmTLSの設定を行なっていきます。それ以外は通常のHTTPSを使用するときと同様です。
以下はALBに対してaddListener
でリスナーを追加していますが、もちろんリスナーをnewする形でも定義可能です。
// リスナーを追加 lb.addListener('Listener', { port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, certificates: [certificate], // mTLSの設定 mutualAuthentication: { ignoreClientCertificateExpiry: false, // 期限切れのクライアント証明書を無視するか。セキュリティを強固にするのであればtrue推奨 mutualAuthenticationMode: elbv2.MutualAuthenticationMode.VERIFY, // トラストストアモードの場合はVERIFYを指定 trustStore, // トラストストアを指定 }, defaultAction: elbv2.ListenerAction.fixedResponse(200, { contentType: 'text/plain', messageBody: 'Success mTLS' }), }); // リスナーをnewする場合 new elbv2.ApplicationListener(this, 'Listener', { loadBalancer: lb, port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, certificates: [certificate], mutualAuthentication: { ignoreClientCertificateExpiry: false, mutualAuthenticationMode: elbv2.MutualAuthenticationMode.VERIFY, trustStore, }, defaultAction: elbv2.ListenerAction.fixedResponse(200, { contentType: 'text/plain', messageBody: 'Success mTLS' }), });
4. CRL (Certificate Revocation List)を設定(任意)
クライアント証明書のCRL(証明書失効リスト)を作成して、トラストストアに追加することが可能です。
トラストストアとほぼ同様で、.pem
を作成してS3バケットに配置しておき、それを参照させる形で定義します。
// CRLを設定 const trustStoreRevocation = new elbv2.TrustStoreRevocation(this, 'Revocation', { trustStore, // トラストストアを指定 revocationContents: [ { bucket, key: 'crl.pem', }, ], }); // BucketDeploymentを使用する場合は依存関係を定義 trustStoreRevocation.node.addDependency(deploy);
以上がALBのmTLS設定(トラストストアモード)の全量です。
コントリビュート時に困ったこと
特殊な点がいくつかあり、手こずった点をまとめておきます。
ドメインを使ったinteg testが必要
HTTPSの設定が必要なので、テスト用のドメインを持っていないとコントリビュート自体ができません(integ testが実行できない)。私は物好きなのでお名前.comでテスト用のドメインを取得してコントリビュートしました。
ドメインを取得した上でドメイン名は実装やテスト結果に残さない形で実装します。詳細はframework-integのドキュメントにも書いてあります。
以下実装方法などを記載します。※ドキュメントや実装を調査した内容をもとに記載していますが、若干自信ないところもあるため間違ってるところあれば教えてください。
まず、ホストゾーンID、ホストゾーン名、ドメイン名は以下のように環境変数で渡す形にします。以下は今回使用した実装ですが、他のinteg testのものからコピペで持ってきています。
CDK_INTEG_...
の環境変数はrunner-baseで定義されている値です。最終的にこの値がsynthで生成されるCFnテンプレートに残ります(自前のドメイン名等はinteg testの結果に残らない形になります)
/** * ホストゾーンID、ホストゾーン名、ドメイン名は以下のように環境変数で渡す * */ const hostedZoneId = process.env.CDK_INTEG_HOSTED_ZONE_ID ?? process.env.HOSTED_ZONE_ID; if (!hostedZoneId) throw new Error('For this test you must provide your own HostedZoneId as an env var "HOSTED_ZONE_ID". See framework-integ/README.md for details.'); const hostedZoneName = process.env.CDK_INTEG_HOSTED_ZONE_NAME ?? process.env.HOSTED_ZONE_NAME; if (!hostedZoneName) throw new Error('For this test you must provide your own HostedZoneName as an env var "HOSTED_ZONE_NAME". See framework-integ/README.md for details.'); const domainName = process.env.CDK_INTEG_DOMAIN_NAME ?? process.env.DOMAIN_NAME; if (!domainName) throw new Error('For this test you must provide your own DomainName as an env var "DOMAIN_NAME". See framework-integ/README.md for details.'); const app = new App(); const stack = new MutualTls(app, 'alb-mtls-test-stack', { hostedZoneId, // propsで渡す hostedZoneName, domainName, });
その上でinteg testにおいて、enableLookups
をtrue
に、stackUpdateWorkflow
をfalse
にすることでLookupを有効にしつつ、CFnテンプレートにはダミーの値が埋め込まれるようになります(自前のドメイン名などは残らず、process.env.CDK_INTEG_DOMAIN_NAME
に定義されたexample.com
がテンプレートに残るなど)。
new IntegTest(app, 'alb-mtls-integ', { testCases: [stack], enableLookups: true, stackUpdateWorkflow: false, });
あとはinteg test実行前に、自前のホストゾーンやドメイン名を環境変数を設定して走行させるだけです。
export HOSTED_ZONE_ID=your_hosted_zone_id export HOSTED_ZONE_NAME=your_hosted_zone_name export DOMAIN_NAME=your_domain_name
integ testに必要なCA証明書などはコミットしたくない
証明書などはセキュリティ面を踏まえると、できればリポジトリに入れたく無いところです。しかしinteg testを走行するためには必要なので、再走行用が必要になった時のための手段は残す必要があります。
今回はinteg testの冒頭にCA証明書などの発行含めて、integ testを走行するための手順を全て記載することで対応しました。
リソースにおいてnameに相当するプロパティが無い場合の対処
CRLの設定をするためTrustStoreRevocationのCloudFormationにおけるプロパティを見てみます。
以下の通りリソース名を表すプロパティ(...name
のような)が存在しません。
Type: AWS::ElasticLoadBalancingV2::TrustStoreRevocation Properties: RevocationContents: - RevocationContent TrustStoreArn: String
じゃあL2 Construct化する時も定義しなくてええか、ということで正直に実装するとビルドで以下のエラーを吐きます。
error: [awslint:props-physical-name:aws-cdk-lib.aws_elasticloadbalancingv2.TrustStoreRevocationProps] Every Resource must have a single physical name construction property, with a name that is an ending substring of <cfnResource>Name
props-physical-nameのリンターで、nameに相当するプロパティが存在しないため弾かれています。しかし今回は該当するプロパティがCloudFormationテンプレートに存在しないためどうしようもありません。
その場合は aws-lint.jsonにチェック除外とするように設定します。
今回はprops-physical-name
のチェックを、TrustStoreRevocationのプロパティであるTrustStoreRevocationProps
に対しては適用しないよう、除外設定をしています。
{ "exclude": [ //omit "props-physical-name:aws-cdk-lib.aws_elasticloadbalancingv2.TrustStoreRevocationProps", // omit ] }
おわりに
今回のL2対応により、ALBのmTLS対応はかなり使いやすくなったかと思います。ぜひ使ってみてください!