はじめに
CDKでAWSのリソースを作成することをよくやっているのですが、最近ベストプラクティスに沿っているかどうかを機械的にチェックできないかという話を受けました。調べたところ、cdk-nagというものを見つけました。
少し古いですがAWSのブログでも紹介されています。
せっかくなのでちょっと試してみました。
環境
以下の環境で試しました。
- AWS CDK 2.1014.0
- cdk-nag 2.35.95
言語はTypeScriptです。
環境構築
環境構築は、npm install cdk-nag
でインストールしたのち、bin
以下にあるCDKアプリを修正します。コード中のCdktestStack
は適宜修正してください。ポイントはcdk.Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
の1行です。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import { CdktestStack } from '../lib/cdktest-stack'; import { AwsSolutionsChecks } from 'cdk-nag'; const app = new cdk.App(); cdk.Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); new CdktestStack(app, 'CdktestStack', {});
これでnpm run cdk synth
の実行時にcdk-nagのチェックが走ります。チェックのルールは以下のリンク先に書かれていますが、数が多いので経験を積んで行った方が良いかなと思います。
単純にS3バケットを作成する以下のコードで動かしてみます。
export class CdktestStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const bucket = new Bucket(this, 'Bucket', {}); } }
以下の2つのエラーが出ました。
[Error at /CdktestStack/Bucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled. The bucket should have server access logging enabled to provide detailed records for the requests that are made to the bucket. [Error at /CdktestStack/Bucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL. You can use HTTPS (TLS) to help prevent potential attackers from eavesdropping on or manipulating network traffic using person-in-the-middle or similar attacks. You should allow only encrypted connections over HTTPS (TLS) using the aws:SecureTransport condition on Amazon S3 bucket policies. Found errors
ここで指摘されているサーバーアクセスログとHTTPSの強制はS3に対するセキュリティベストプラクティスに記載があるものになっています。
テスト(Jest)実行時のチェック
npm run cdk synth
でやるのもよいのですが、テスト(Jest)実行時にチェックすることをやってみます。こんな実装を行います。
import * as cdk from 'aws-cdk-lib'; import { AwsSolutionsChecks } from 'cdk-nag'; import { CdktestStack } from '../lib/cdktest-stack'; import { Annotations, Match } from 'aws-cdk-lib/assertions'; describe('cdknag', () => { let stack: cdk.Stack; let app: cdk.App; beforeAll(() => { app = new cdk.App stack = new CdktestStack(app, 'MyTestStack'); cdk.Aspects.of(stack).add(new AwsSolutionsChecks()); }); test("No unsuppressed Warnings", () => { const warnings = Annotations.fromStack(stack).findWarning( "*", Match.stringLikeRegexp("AwsSolutions-.*"), ); expect(warnings).toHaveLength(0); }); test("No unsuppressed Errors", () => { const errors = Annotations.fromStack(stack).findError( "*", Match.stringLikeRegexp("AwsSolutions-.*"), ); expect(errors).toHaveLength(0); }); });
これでnpm run test
を実行するとテストが失敗してエラーメッセージが出力されます。
FAIL test/cdktest.test.ts cdknag ✓ No unsuppressed Warnings (207 ms) ✕ No unsuppressed Errors (19 ms) ● cdknag › No unsuppressed Errors expect(received).toHaveLength(expected) Expected length: 0 Received length: 2 Received array: [{"entry": {"data": "AwsSolutions-S1: The S3 Bucket has server access logs disabled. (以下省略)
今回省略した部分に何が原因でテストが失敗したかはよく読むとわかるようになっているのですが、ちょっと見にくいのが正直なところです。
修正
チェックで指摘されたからには修正をする必要があります。今回、以下のように修正しました。
(上記略) const bucket = new Bucket(this, 'Bucket', { serverAccessLogsBucket: new Bucket(this, 'Log', { enforceSSL: true }), serverAccessLogsPrefix: 'log/', enforceSSL: true }); (以下略)
修正すると、npm run cdk synth
はAWS CloudFormation テンプレートを出力し、npm run test
ではテストが成功します。
# npm run cdk synthの結果 Resources: Log95422804: Type: AWS::S3::Bucket UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: CdktestStack/Log/Resource LogPolicy4B9D4AF7: Type: AWS::S3::BucketPolicy (以下略) # npm run testの結果 PASS test/cdktest.test.ts cdknag ✓ No unsuppressed Warnings (198 ms) ✓ No unsuppressed Errors (27 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 5.048 s Ran all test suites.
EC2を作成する
次にEC2を作成する以下のコードで試してみます。
const vpc = new Vpc(this, 'Vpc'); const securityGroupForEC2 = new SecurityGroup(this, 'SecurityGroupForEC2', { vpc: vpc, securityGroupName: 'SecurityGroupForEC2', allowAllOutbound: true, }); // SSHを許可 securityGroupForEC2.addIngressRule( // 任意のIPアドレスからのアクセスを許可 Peer.anyIpv4(), // SSHのポートを許可 Port.tcp(22), 'Allow SSH from anywhere', ); // EC2インスタンスを作成する new Instance(this, 'Instance', { vpc: vpc, vpcSubnets: { subnets: vpc.publicSubnets }, securityGroup: securityGroupForEC2, instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO), machineImage: MachineImage.latestAmazonLinux2023(), });
結果としては以下のものが出力されました。
[Error at /CdktestStack/Vpc/Resource] AwsSolutions-VPC7: The VPC does not have an associated Flow Log. VPC Flow Logs capture network flow information for a VPC, subnet, or network interface and stores it in Amazon CloudWatch Logs. Flow log data can help customers troubleshoot network issues; for example, to diagnose why specific traffic is not reaching an instance, which might be a result of overly restrictive security group rules. [Error at /CdktestStack/SecurityGroupForEC2/Resource] AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. Large port ranges, when open, expose instances to unwanted attacks. More than that, they make traceability of vulnerabilities very difficult. For instance, your web servers may only require 80 and 443 ports to be open, but not all. One of the most common mistakes observed is when all ports for 0.0.0.0/0 range are open in a rush to access the instance. EC2 instances must expose only to those ports enabled on the corresponding security group level. [Error at /CdktestStack/Instance/Resource] AwsSolutions-EC26: The resource creates one or more EBS volumes that have encryption disabled. With EBS encryption, you aren't required to build, maintain, and secure your own key management infrastructure. EBS encryption uses KMS keys when creating encrypted volumes and snapshots. This helps protect data at rest. [Error at /CdktestStack/Instance/Resource] AwsSolutions-EC28: The EC2 instance/AutoScaling launch configuration does not have detailed monitoring enabled. Monitoring data helps make better decisions on architecting and managing compute resources. [Error at /CdktestStack/Instance/Resource] AwsSolutions-EC29: The EC2 instance is not part of an ASG and has Termination Protection disabled. Termination Protection safety feature enabled in order to protect the instances from being accidentally terminated. Found errors
セキュリティグループの設定で0.0.0.0/0
を許可していることを検出しているのは喜ぶ人も多いかなと思います。
一方でAwsSolutions-EC29
の対応については悩ましいところです。L2コンストラクタではTermination Protectionを設定する方法がなく、L1コンストラクタのみ設定方法があります。
このため、このチェックを抑制するには以下のように実装します。
const instance = new Instance(this, 'Instance', { vpc: vpc, vpcSubnets: { subnets: vpc.publicSubnets }, securityGroup: securityGroupForEC2, instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO), machineImage: MachineImage.latestAmazonLinux2023(), }); NagSuppressions.addResourceSuppressions(instance, [ { id: 'AwsSolutions-EC29', reason: 'This is a test environment.' } ]);
NagSuppressions.addResourceSuppressions
でチェックを抑制できます。
このため、多くのチェックが実施可能ですが必要であればチェックを抑制するとよいかと考えています。
考察
cdk-nagを使ったCDKの検証を実施しました。簡単な考察を以下に記します。
cdk synth
とテスト(Jest)を使った両方のテストを実装するのが好ましい- テスト(Jest)は動かさない人も多く、
cdk synth
で問題があるコードを実行させない取り組みは有用と考えるため - 日々の開発ではテスト(Jest)で動く/動かないを早めに確認することになるかと考えています
- テスト(Jest)の結果が見にくいのは要改善
- テスト(Jest)は動かさない人も多く、
- エラー等を出力させないように抑制させることも可能ですが、極力避けた方が良いかと思います
- 結局抑制だらけになるため
- 一方で、若干厳しすぎる面もあり必要であれば抑制するとよいかと。ケースバイケースになるので判断が難しいのですが。