NRIネットコム Blog

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

cdk-nagを使用したAWS CDKのセキュリティチェック ~独自ルール作成~

こんにちは、上野です。

前回の続きです。前回はcdk-nagの基本的な使い方を紹介しました。

AWS Solutionsのルール群を見て思ったのは、少し厳しすぎる内容も多いかも?という点です。特に既存環境にcdk-nagを導入した場合、多くのErrorを検知するかもしれません。多くのErrorを検知するとその抑制対応に時間も取られることになり、抑制分のコード量も増えてしまいます。

そこで今回は、任意のルールをピックアップしてオリジナルのルール群を作成する方法を試してみます。

独自のルール群を作成する

cdk-nagが提供しているルール群は、NagPackという仕組みが使われており、同じ仕組みを使用して独自のルール群を作成できます。GithubのNagPack.mdでも紹介されています。

前回とは異なる新規CDKアプリケーションを作成します。cdk-nagもinstallしておきます。

$ mkdir cdk-nag-myrule && cd cdk-nag-myrule
$ cdk init app --language typescript
$ npm install cdk-nag

独自ルールを作成してきます。 今回は私の独断で以下のルールをピックアップしてみました。ルールの一覧はGithubのrulesで確認できます。

ルール名 概要
EC2RestrictedInbound Security Groupの公開禁止
S3BucketPublicWriteProhibited S3バケットの書き込み公開禁止
RDSInstancePublicAccess RDSのパブリックインスタンス禁止
ECROpenAccess ECRリポジトリの公開禁止
S3BucketServerSideEncryptionEnabled S3のデフォルト暗号化チェック
EC2EBSVolumeEncrypted EBSのデフォルト暗号化チェック
RDSStorageEncrypted RDSストレージのデフォルト暗号化チェック
VPCFlowLogsEnabled VPC Flow Logs有効チェック
CloudFrontDistributionAccessLogging CloudFrontアクセスログ有効チェック
ELBLoggingEnabled ELBのログ有効チェック
IAMNoWildcardPermissions IAMポリシーのワイルドカード禁止

実装していきます。bin 配下にmyrule-checks.tsというファイルを新規作成します。 ファイル内容は以下のとおりです。infoやexplainなどの表示文言は、AWS Solutionsの内容を参考にしました。
※長いので初期表示は折りたたんでいます。

作成したルールがチェックされるよう、bin/cdk-nag-myrule.tsに以下のように追加しておきます。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkNagMyruleStack } from '../lib/cdk-nag-myrule-stack';
import { MyruleChecks } from './myrule-checks' //追加

const app = new cdk.App();
new CdkNagMyruleStack(app, 'CdkNagMyruleStack', {});
cdk.Aspects.of(app).add(new MyruleChecks()); //追加

この状態で、lib/cdk-nag-myrule-stack.tsにリソースを定義していきます。

import { Size, 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 s3  from 'aws-cdk-lib/aws-s3'

export class CdkNagMyruleStack 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'))
    role.addToPolicy(
      new iam.PolicyStatement({
        actions: ['s3:*'],
        resources:['*'],
    }))

    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,
    })

    const bucket = new s3.Bucket(this, 'bucket')

  }
}

構成は以下の通りです。すべてのルールを検知させるわけではないですが、VPCがFlow Logs無し、SGがSSH公開、バケットが暗号化無し、IAMロールがs3:*という強い権限を持つ、といったいくつかルール違反の設定があります。

この状態でcdk deploy (もしくはcdk synth)を実行すると、以下のように検知します。

[Error at /CdkNagMyruleStack/vpc/Resource] Myrule-VPCFlowLogsEnabled: The VPC does not have an associated Flow Log.

[Error at /CdkNagMyruleStack/securityGroup/Resource] Myrule-EC2RestrictedInbound: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access.

[Error at /CdkNagMyruleStack/role/DefaultPolicy/Resource] Myrule-IAMNoWildcardPermissions[Action::s3:*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission.

[Error at /CdkNagMyruleStack/role/DefaultPolicy/Resource] Myrule-IAMNoWildcardPermissions[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission.

[Error at /CdkNagMyruleStack/bucket/Resource] Myrule-S3BucketPublicWriteProhibited: The S3 Bucket does not have public write access restricted and blocked.

[Error at /CdkNagMyruleStack/bucket/Resource] Myrule-S3BucketServerSideEncryptionEnabled: The S3 Bucket does not default encryption enabled.


Found errors

自分が選んで設定したルールだけ検知できました。意図的な設定でエラーを消したい場合は抑制(Suppressing)すればOKです。詳細は前回の記事に書いています。

ここでやってみて気づいたのですが、「EBSのデフォルト暗号化チェック」がEC2インスタンスに対してできていません。EBS Volumeを個別に定義した場合に、暗号化オプションをチェックする仕組みになっており、EC2インスタンスの定義として設定されたEBSボリュームはチェックされませんでした。(=暗号化無しでもErrorにならない)

ということでこの部分について独自のルールを作成してみます。

独自ルールを作成する

先ほどのコードでは、rulesから好きなものを選んで書いた程度でした。

NagPackと既存のルールの書き方を参考に、独自ルールを追加します。

bin/myrule-checks.tsに書いていきます。

まず上部に以下のように追加します。

import { CfnResource, Stack } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
import {
    NagMessageLevel,
    NagPack,
    NagPackProps,
    NagRuleCompliance, //追加
    NagRuleResult, //追加
    NagRules, //追加
    rules,
  } from 'cdk-nag';
import { CfnInstance } from 'aws-cdk-lib/aws-ec2'; //追加

既存のルールをたくさん書いているところに、1つルールを以下のように追加します。

        //独自ルール EBS暗号化チェック
        this.applyRule({
          ruleSuffixOverride: 'EBSVolumeEncryptedWithEC2',
          info: 'The EBS volume with the instance has encryption disabled.',
          explanation:
            "EBS encryption uses KMS keys when creating encrypted volumes and snapshots. This helps protect data at rest.",
          level: NagMessageLevel.ERROR,
          rule: function (node2: CfnResource): NagRuleResult {
            if (node2 instanceof CfnInstance) {
              const blockDevices = Stack.of(node2).resolve(node2.blockDeviceMappings);
              if (blockDevices != undefined) {
                //Iterate through blockDevices checking if encryption is not enabled
                for (const blockDevice of blockDevices) {
                  const resolvedDevice = Stack.of(node).resolve(blockDevice);
                  const encryption = NagRules.resolveIfPrimitive(
                    node,
                    resolvedDevice.ebs.encrypted
                  );
                  if (encryption != true) {
                    return NagRuleCompliance.NON_COMPLIANT;
                  }
                }
                return NagRuleCompliance.COMPLIANT;
              }
              return NagRuleCompliance.NON_COMPLIANT;
            } else {
              return NagRuleCompliance.NOT_APPLICABLE;
            }
          },
          node: node,
        }); 

※あくまでサンプルとして参考程度に書いているコードです。動作を保証するものではないのでご認識ください。

中身としては、EC2内で定義されたブロックデバイス一覧にそれぞれ暗号化設定(encrypted)が入っているか確認し、入ってないものが1つでもあればNON_COMPLIANTになるようにしています。

この状態で、再度cdk deployを実行してみます。すると先ほどは無かったEBS暗号化が無効になっている旨のメッセージが表示されます。(他のメッセージは省略)

[Error at /CdkNagMyruleStack/instance/Resource] Myrule-EBSVolumeEncryptedWithEC2: The EBS volume with the instance has encryption disabled.

以下のようにEC2の定義にblockDevicesおよびencrypted: trueを定義して意図的に暗号化設定すればエラーは解消します。

    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,
      blockDevices: [
        {
          deviceName: '/dev/xvda',
          mappingEnabled: true,
          volume: ec2.BlockDeviceVolume.ebs(30, {
            deleteOnTermination: true,
            volumeType: ec2.EbsDeviceVolumeType.GP2,
            encrypted: true,
          })
        },
      ],
    })

独自ルールの検証は以上です。

まとめ

独自のルール群と、ルールそのものも独自で作成してみました。ここまで自由度が高ければ、ワークロードごとの独自のルールや設計チェックに活用できそうです。独自ルールについては、あまり作りこみすぎても管理が大変なので、基本は用意されているルールを使えば良いかなと思いました。必要最低限のルールから始めて、導入すすめていきたいですね。

執筆者上野史瑛

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