NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

cdk-nagを使用したAWS CDKのセキュリティチェック ~基本編~

こんにちは、上野です。

Infrastructure as Code (IaC) 、みなさん楽しんでおりますでしょうか。前から気になっていたcdk-nagを試してみたので、その紹介となります。

cdk-nagとは

AWS Cloud Development Kit (AWS CDK) で作成する各Constructが、与えられたセキュリティ・コンプライアンスルール群に準拠しているかどうか検証してくれるツールです。CloudFormationテンプレートのセキュリティチェックツールである、cfn-nagに影響を受け作成されています。現時点では以下のルール群が提供されています。

  1. AWS Solutions
  2. HIPAA Security
  3. NIST 800-53 rev 4
  4. NIST 800-53 rev 5
  5. PCI DSS 3.2.1

GitHubリポジトリで公開されており、AWS公式ブログでも使い方が紹介されています。

cdk-nagの嬉しいポイント

cdk-nagの仕組み紹介の前に、嬉しいポイントをあげておきます。

AWS環境へデプロイ時にセキュリティチェックができる

みなさまはAWS環境のセキュリティチェックはどうやっていますでしょうか?私の担当範囲ではAWSアカウント側にAWS Configルールを設定して危険なリソース設定を検知するパターンが多いです。

このパターンでは、検知⇒確認⇒修正まで一定の時間がかかるので、一時的に危険な設定が残るというリスクが発生します。Lambdaで自動修復処理を設定し、極力リアルタイムで修正するというパターンもありますが、CDKやCloudFormationなどのIaCの定義とずれが発生する等の問題もあったり、可能であれば危険な設定状態を作らないというのが理想です。

明らかにNGな設定はService control policy(SCP)やPermissions Boundaryを使用して強制的に操作を拒否してしまうというのも手ですが、細かい設定値を条件に書くことが難しいことも多いです。

AWS環境にデプロイ時(環境反映前)に危険な設定をチェックして防げたほうが安全です。cdk-nagを使うと事前チェックができます!

※cdk-nagで導入しても、手動操作など別の場所から危険な設定がされる場合もありますので、Configルールの運用が不要になるわけではありません。

CDKのコードに埋め込むことで、ほぼ強制的にセキュリティチェックを実行できる

これはcfn-nagに比べて嬉しいポイントです。cfn-nagは基本的にコマンドを実行してCloudFormationテンプレートのチェックを行うので、強制的に実行するにはCIツール等に組み込んでチェックする仕組みが必要で、手動デプロイ時はチェックコマンドを忘れてしまう可能性もありました。cdk-nagの場合はcdk synthcdk deploy時に自動チェックしてくれるので、共同で管理するコードに入れておけば、開発者がコメントアウトしない限りチェックを忘れることは無いでしょう。

ルールに非準拠のリソースは理由を書くことが強制され、例外リソースが管理しやすい

これは後ほど紹介する動作を見ていただければわかりますが、例外リソース(チェックを抑制するリソース)に対してなぜ例外なのかを記載することが強制されます。

設計する際に、「0.0.0.0/0インバウンド通信を許可するSecurity Groupを作成しない」というルールを設け、「ただしインタンスAは外部の不特定IPからXXポートでアクセスする要件があるため例外とする」のような、設計したルールに対し例外が出てくるパターンが出てきます。こういった場合はドキュメントやIaCのコメント部分に記載して管理することになりますが、cdk-nagでは例外リソースに明示的に理由(reason)項目があるためこういった管理に役立ちます。

cdk-nagの初期設定とサンプルリソース作成

実際に動作を見ていきます。 まずはcdkでサンプルリソースを作成します。

$ mkdir cdk-nag-sample && cd cdk-nag-sample
$ cdk init app --language typescript

cdk-nagをインストールします。

$ npm install cdk-nag

bin/cdk-nag-sample.tsに以下のように追記します。今回はAWS Solutionsのルール群を使用します。cdk-nagの準備はこれでOKです。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkNagSampleStack } from '../lib/cdk-nag-sample-stack';
// 以下2行追記
import { AwsSolutionsChecks } from 'cdk-nag'
import { Aspects } from 'aws-cdk-lib';

const app = new cdk.App();
new CdkNagSampleStack(app, 'CdkNagSampleStack', {});
// 追記
Aspects.of(app).add(new AwsSolutionsChecks());

lib/cdk-nag-sample-stack.tsにリソースを定義していきます。今回は以下の構成で、少しよろしくない設定も含めEC2インスタンスとVPCを作成してみます。

コードは以下のとおりです。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';

export class CdkNagSampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

      const vpc = new ec2.Vpc(this, 'vpc', {
        natGateways: 0,
        subnetConfiguration: [
          {
            cidrMask: 24,
            name: 'public',
            subnetType: ec2.SubnetType.PUBLIC,
          },
        ]
      })

      const securityGroup = new ec2.SecurityGroup(this, 'securityGroup', {
        vpc,
        allowAllOutbound: true,
      })
      securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'SSH Open!!')

      const role = new iam.Role(this, 'role', {
        assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
      })
      role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'))

      const instance = new ec2.Instance(this, 'instance', {
        vpc,
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO),
        machineImage: new ec2.AmazonLinuxImage({
          generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
          cpuType: ec2.AmazonLinuxCpuType.ARM_64,
        }),
        role: role,
      })

  }
}

チェック実行

この状態でcdk deployを実行すると、以下のようにcdk-nagのエラーが表示されます。(cdk synthでも同じです。)

[Error at /CdkNagSampleStack/vpc/Resource] AwsSolutions-VPC7: The VPC does not have an associated Flow Log.

[Error at /CdkNagSampleStack/securityGroup/Resource] AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access.

[Error at /CdkNagSampleStack/role/Resource] AwsSolutions-IAM4[Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonSSMManagedInstanceCore]: The IAM user, role, or group uses AWS managed policies.

[Error at /CdkNagSampleStack/instance/Resource] AwsSolutions-EC28: The EC2 instance/AutoScaling launch configuration does not have detailed monitoring enabled.

[Error at /CdkNagSampleStack/instance/Resource] AwsSolutions-EC29: The EC2 instance is not part of an ASG and has Termination Protection disabled.


Found errors

ターミナル(VS Code)上だと真っ赤です。

エラーについて、補足説明や詳細も確認したい場合はbin/cdk-nag-sample.tsの最後の部分を以下のように修正することで詳細なメッセージを表示できます。

Aspects.of(app).add(new AwsSolutionsChecks({ verbose:true }));

表示メッセージ(詳細版)は以下のとおりです。なぜ良くないのかも書かれているのは良いですね。

[Error at /CdkNagSampleStack/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 /CdkNagSampleStack/securityGroup/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 /CdkNagSampleStack/role/Resource] AwsSolutions-IAM4[Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonSSMManagedInstanceCore]: The IAM user, role, or group uses AWS managed policies. An AWS managed policy is a standalone policy that is created and administered by AWS. Currently, many AWS managed policies do not restrict resource scope. Replace AWS managed policies with system specific (customer) managed policies.This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Policy::<policy>' for AWS managed policies. Example: appliesTo: ['Policy::arn:<AWS::Partition>:iam::aws:policy/foo'].

[Error at /CdkNagSampleStack/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 /CdkNagSampleStack/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

エラー対応

表示されているエラーの概要は以下のとおりです。対応列の方針で修正してきます。

Rule ID 概要 対応
AwsSolutions-VPC7 Flow LogがONになっていない Flow LogをONにする
AwsSolutions-EC23 Security Groupで0.0.0.0/0が許可されている 許可を特定IPにする
AwsSolutions-IAM4 Managed Policyが使用されている エラー抑制
AwsSolutions-EC28 EC2のdetailed monitoringが有効になっていない エラー抑制
AwsSolutions-EC29 EC2のTermination Protectionが無効 エラー抑制

Flow LogをONにする

以下の内容をlib/cdk-nag-sample-stack.tsに追加して、Flow LogをCloudWatch Logsに送信するようにします。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs' // 追加

// ~省略~ 以下追加部分

      const logGroup = new logs.LogGroup(this, 'logGroupFlowLog');

      const logRole = new iam.Role(this, 'logRole', {
        assumedBy: new iam.ServicePrincipal('vpc-flow-logs.amazonaws.com')
      });

      new ec2.FlowLog(this, 'flowLog', {
        resourceType: ec2.FlowLogResourceType.fromVpc(vpc),
        destination: ec2.FlowLogDestination.toCloudWatchLogs(logGroup, logRole)
      });

// ~省略~ 

Security Groupの許可を特定IPにする

IP許可部分を以下のように特定のIPに変更します。※記載の値はドキュメント用IPアドレスです。

securityGroup.addIngressRule(ec2.Peer.ipv4('203.0.113.0/24'), ec2.Port.tcp(22), 'SSH Open!!')

抑制(Suppressing)

残り3つのErrorは意図的とし、エラーが出ないよう抑制していきます。READMEにもやり方がいくつか紹介されています。

READMEの「Example 1) Default Construct」で紹介されているConstruct単位で抑制するやり方で抑制していきます。

まずはNagSuppressionsを使用できるよう以下のとおりlib/cdk-nag-sample-stack.tsに追加しておきます。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs'
import { NagSuppressions } from 'cdk-nag'; //追加

検知しているリソースごとに、検知しているIDと理由(reason)を記載していく形式です。

  • AwsSolutions-IAM4 (マネージドポリシー使用)
      const role = new iam.Role(this, 'role', {
        assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
      })
      role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'))
      // 追記
      NagSuppressions.addResourceSuppressions(role,[
        {id: 'AwsSolutions-IAM4', reason:'use aws recommended policy for seesion manager'}
      ])
  • AwsSolutions-EC28(detailed monitoring無効)と AwsSolutions-EC29(Termination Protection無効)

この2つはどちらもEC2インスタンスに対するチェックなので合わせて記載します。

      const instance = new ec2.Instance(this, 'instance', {
        vpc,
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO),
        machineImage: new ec2.AmazonLinuxImage({
          generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
          cpuType: ec2.AmazonLinuxCpuType.ARM_64,
        }),
        role: role,
      })
      // 追記
      NagSuppressions.addResourceSuppressions(instance,[
        {id: 'AwsSolutions-EC28', reason:'dev instance do not require detailed monitorng'}
      ])
      NagSuppressions.addResourceSuppressions(instance,[
        {id: 'AwsSolutions-EC29', reason:'dev instance do not require Termination Protection'}
      ])

これで対応は完了です。再びcdk deployを実行してみます。

$ cdk deploy

✨  Synthesis time: 5.01s

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────┬────────┬──────────────────────────────────────────────────────────────┬─────────────────────────────────────┬───────────┐
│   │ Resource               │ Effect │ Action                                                       │ Principal                           │ Condition │
├───┼────────────────────────┼────────┼──────────────────────────────────────────────────────────────┼─────────────────────────────────────┼───────────┤
│ + │ ${logGroupFlowLog.Arn} │ Allow  │ logs:CreateLogStream                                         │ AWS:${logRole}                      │           │
│   │                        │        │ logs:DescribeLogStreams                                      │                                     │           │
│   │                        │        │ logs:PutLogEvents                                            │                                     │           │
├───┼────────────────────────┼────────┼──────────────────────────────────────────────────────────────┼─────────────────────────────────────┼───────────┤
│ + │ ${logRole.Arn}         │ Allow  │ sts:AssumeRole                                               │ Service:vpc-flow-logs.amazonaws.com │           │
│ + │ ${logRole.Arn}         │ Allow  │ iam:PassRole                                                 │ AWS:${logRole}                      │           │
├───┼────────────────────────┼────────┼──────────────────────────────────────────────────────────────┼─────────────────────────────────────┼───────────┤
│ + │ ${role.Arn}            │ Allow  │ sts:AssumeRole                                               │ Service:ec2.${AWS::URLSuffix}       │           │
└───┴────────────────────────┴────────┴──────────────────────────────────────────────────────────────┴─────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────┬────────────────────────────────────────────────────────────────────┐
│   │ Resource │ Managed Policy ARN                                                 │
├───┼──────────┼────────────────────────────────────────────────────────────────────┤
│ + │ ${role}  │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore │
└───┴──────────┴────────────────────────────────────────────────────────────────────┘
Security Group Changes
┌───┬───────────────────────────────────────────┬─────┬────────────┬─────────────────┐
│   │ Group                                     │ Dir │ Protocol   │ Peer            │
├───┼───────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${instance/InstanceSecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │
├───┼───────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${securityGroup.GroupId}                  │ In  │ TCP 22203.0.113.0/24  │
│ + │ ${securityGroup.GroupId}                  │ Out │ Everything │ Everyone (IPv4) │
└───┴───────────────────────────────────────────┴─────┴────────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? 

無事実行できるようになりました!

抑制方法の補足

抑制の方法は、以下ようにStack全体で設定する方法もあります。

  • bin/cdk-nag-sample.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkNagSampleStack } from '../lib/cdk-nag-sample-stack';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'
import { Aspects } from 'aws-cdk-lib';

const app = new cdk.App();
const stack = new CdkNagSampleStack(app, 'CdkNagSampleStack', {});
Aspects.of(app).add(new AwsSolutionsChecks({ verbose:true }));

NagSuppressions.addStackSuppressions(stack, [
    { id: 'AwsSolutions-EC29', reason: 'dev instance do not require Termination Protection' },
  ]);

ポリシーの詳細など、より細かい単位で抑制することもできます。(READMEのサンプルより)

import { User, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';

export class CdkTestStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const user = new User(this, 'rUser');
    user.addToPolicy(
      new PolicyStatement({
        actions: ['s3:PutObject'],
        resources: ['arn:aws:s3:::bucket_name/*'],
      })
    );
    // Enable adding suppressions to child constructs
    NagSuppressions.addResourceSuppressions(
      user,
      [
        {
          id: 'AwsSolutions-IAM5',
          reason: 'lorem ipsum',
          appliesTo: ['Resource::arn:aws:s3:::bucket_name/*'], // optional
        },
      ],
      true
    );
  }
}

後半へ続く

独自のルール群を作成する方法も紹介したかったのですが、基本機能の紹介だけでけっこうなボリュームになったので、今回は一旦ここまでとします。独自ルール群の話は別記事にしたいと思います。

執筆者上野史瑛

Japan APN Ambassador
AWSを中心としたクラウドの導入、最適化を専門に行っています。