CDK Aspects・Nag本番導入3ヶ月で気づいた、自動セキュリティ検証の現実
CDK AspectsとNagを導入してみたら、False Positiveの対処が意外と大変だった。チーム運用で見えた落とし穴と実装パターンを3ヶ月の経験から解説します。
CDK Aspects・Nagで本番導入してみた
うちのチームで先月、CDK AspectsとAWS Nagを本番環境に組み込んだんですよ。正直「これで自動でセキュリティチェックが走って楽になるな」って思ってたんですが、3ヶ月運用してみると地味だけど重い課題がいっぱい出てきた。今日はその実装記録を共有したいと思います。
実は導入する前、うちはセキュリティレビューって人手でやってたんです。デプロイするたびに「このIAMポリシー大丈夫?」「S3のパブリックアクセス許可されてない?」みたいなのをチェックリストで確認してた。これが時間かかるし、漏れるし、最悪でした。
Nagで何ができるのか、実際に試してみた
NagはAWS製のセキュリティルール検査ツールで、CloudFormationテンプレートに対して事前定義されたルールセットを実行します。AWS Construct Library(CDK)と組み合わせると、デプロイ前にセキュリティ違反を検出できるんですね。
うちが導入した構成はこんな感じです:
import { Aspects, Stack } from 'aws-cdk-lib';
import { AwsSolutions } from 'cdk-nag';
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// S3バケット作成
const bucket = new s3.Bucket(this, 'MyBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
});
// IAMロール
const role = new iam.Role(this, 'MyRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
role.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucket.arnForObjects('*')],
effect: iam.Effect.ALLOW,
})
);
}
}
// Aspectsの適用
const app = new App();
const stack = new MyStack(app, 'MyStack');
Aspects.of(stack).add(new AwsSolutions());
これ、デプロイ時に自動でチェックが走るんですよ。最初は感動しました。でもここからが本当の始まりでした。
実装してから気づいた現実:False Positiveの嵐
導入初日、デプロイを試したら500個以上のエラーが出ました。マジで焦りました。
例えばこんなのが報告されます:
Rule: AwsSolutions-IAM1: IAM policies should not allow full "*" administrative privileges
Resource: MyRole
Message: IAM policy allows full administrative privileges
でもね、これってうちの場合は意図的なんですよ。特定の環境では広い権限が必要な場合があるんです。CloudFormation検証だと「絶対ダメ」になってしまう。
実際に運用して気づいた主な落とし穴があります:
-
IAMワイルドカード検査が厳しすぎる —
s3:GetObjectでresources: ['bucket-name/*']って書くと「ワイルドカードはダメ」って言われる。でも現実的には必要な場合がほとんど -
ログ保持期間 — CloudWatch Logsのデフォルト永続保持がセキュリティ問題扱いされる。これは確かに対応したいけど、すべてのログに明示的に期間指定するとコスト増える
-
VPC設定 — RDSのマルチAZ無効化が引っかかる。開発環境では必要ないのに本番ルールが強制されている
こういった「理屈は正しいんだけど、現実には困る」パターンが大量に出てくるんですよね。
チーム運用で見つけた正解パターン
う〜ん、ここまで試行錯誤した結果、うちは「ルールを段階的に有効化する」戦略に変えました。
import { NagSuppressions } from 'cdk-nag';
// 特定リソースのみ例外処理
NagSuppressions.addResourceSuppressions(role, [
{
id: 'AwsSolutions-IAM1',
reason: 'この環境では広い権限が必要(データレイク用途)',
appliesTo: ['arn:aws:iam::ACCOUNT_ID:role/MyRole'],
},
]);
// スタック全体でルールを無効化
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-RDS1',
reason: '開発環境なのでマルチAZは不要',
},
]);
この方式だと、例外の理由が記録されるから後でレビューしやすいんですよ。デプロイログにも出るし、「なぜこのルール無視してるのか」ってのが履歴に残る。
実装したチェックリスト:何をどの段階で有効化するか
うちは最終的にこういう段階的な導入戦略で落ち着きました:
| 環境 | 有効ルール | 理由 |
|---|---|---|
| 開発 | 基本セキュリティ(S3暗号化、IAM原則など) | 開発速度重視。最小限の強制 |
| ステージング | 本番と同じ(ただし例外あり) | 本番検証のため |
| 本番 | すべてのルール + カスタムルール | 最高レベルのセキュリティ強制 |
設定ファイル方式も導入しました:
{
"nagRulesets": {
"development": {
"enabled": [
"AwsSolutions-S1",
"AwsSolutions-IAM4",
"AwsSolutions-EC2"
]
},
"production": {
"enabled": "all",
"exemptions": [
{
"ruleId": "AwsSolutions-RDS1",
"resourceId": "legacy-db",
"expiresAt": "2026-12-31",
"reason": "レガシーDBの移行予定"
}
]
}
}
}
こうすることで、例外が一時的なものか恒久的なものか明確になるんです。期限を切ることで「いつまでに対応する」ってのが可視化される。個人的には、このファイル方式がチーム全体の判断基準を統一するのに地味に便利でした。
実装してから3ヶ月:チームで運用してわかったこと
あ、一つ大事なのが、CI/CDパイプラインに組み込むタイミングなんです。うちは最初、デプロイの最後に走らせてたんですが、これだとエラーで止まってしまう。結局、デプロイが何度もリトライされるハメに。
いまは「警告レベル」と「エラーレベル」に分けてます:
# CDK synthでNagを実行
cdk synth --context env=production
# 警告レベルはログに出力、エラーレベルは失敗
npm run cdk:nag -- --report-format=json > nag-report.json
カスタムスクリプトで結果を解析するようにしました:
// カスタムスクリプトで結果を解析
const report = require('./nag-report.json');
const errors = report.filter(r => r.severity === 'error');
const warnings = report.filter(r => r.severity === 'warning');
if (errors.length > 0) {
process.exit(1); // デプロイ中止
}
if (warnings.length > 0) {
console.warn(`警告: ${warnings.length}件`);
// 通知を送る
}
こうすることで、本当に危険なセキュリティ違反だけでデプロイをブロックして、軽微な問題は通知しながら進める。実務的ですよね。
本番導入で見えたメリットと課題
ぶっちゃけ、メリットはかなりでかいです。導入後3ヶ月で、手動レビューで見落としていたセキュリティ設定ミスが3件出てきました。以前だったら本番環境で発見されていたはずです。
コード例を一つ:
// BAD: これはNagで引っかかる
const bucket = new s3.Bucket(this, 'DataBucket', {
// publicReadAccess: true, // 不適切
// serverAccessLogsPrefix: undefined, // ログなし
});
// GOOD: Nagで合格する設定
const bucket = new s3.Bucket(this, 'DataBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
serverAccessLogsPrefix: 'access-logs/',
serverAccessLogsBucket: logBucket,
});
ただし課題もあります。一番の課題は「ルールメンテナンスコスト」なんですよ。Nagのルールセット自体がアップデートされるんですが、そのたびに新しい警告が増える。
うちは月1回、ルール更新をレビューする会議を設定しました。どのルールを有効化するか、どれを除外するか。これをドキュメント化して、新しく入ったメンバーに共有する。こういった継続的な対応が案外大変です。
実装・運用の流れを可視化すると
導入から運用までのフローはこんな感じです:
graph LR
A["コード作成"] --> B["CDK synth"]
B --> C["Nagチェック"]
C --> D{エラーあり?}
D -->|yes| E["エラーレベルで失敗"]
E --> F["開発者が修正"]
F --> A
D -->|no| G{警告あり?}
G -->|yes| H["通知・ログ出力"]
G -->|no| I["デプロイ実行"]
H --> I
I --> J["本番環境反映"]
このフローだと、エラーは確実に止めるけど、警告は見守る形になります。
まとめ
CDK Aspects・Nagは確かに強力だけど、運用があるんだってのが実感です。
導入初期は「自動化で楽になる」と思ってたんですが、実際には「セキュリティチェックのやり方が変わる」くらいの認識の方がいいですね。設定ファイルの管理、ルール更新への対応、チーム間でのルール判断の統一…こういったことが必要になってくる。
でも逆に考えると、これらが整備されれば、本当に重要なセキュリティ判断に時間を割けるようになるんです。うちのチームでは導入後、セキュリティレビュー時間が70%削減されました。その分、アーキテクチャレビューとか本質的なセキュリティ検討に人力を回せるようになった。
もし同じようにIaCでセキュリティを強化したいなら、試す価値はあります。ただ「設定して終わり」じゃなく「運用フロー込みで考える」ってのが成功のカギですね。