牙を抜かれたIaC:デプロイ前に攻撃の芽を摘む「ポリシー・アズ・コード」の極意
インフラをコードで定義するInfrastructure as Code(IaC)の普及は、開発速度を爆発的に向上させた。しかし、セキュリティを司る我々の視点から見れば、これは「脆弱性を超高速かつ大規模に自動量産する仕組み」が構築されたことに他ならない。
かつて、手動でサーバーを構築していた時代の設定ミスは「単発の過失」で済んだ。だが、TerraformやAWS CloudFormationのコードに潜むわずか1行のセキュリティ上の不備は、CI/CDパイプラインを通じて瞬時に本番環境全体へデプロイされ、何百ものリソースに複製される。攻撃者は、インターネット上に露出したわずかな隙を狙い、自動化されたスキャナーでポートや公開ストレージを探索している。彼らにとって、設定不備のあるIaCテンプレートは、システム内部へ侵入するための「公式の招待状」なのだ。
本稿では、単なる「設定チェックリストの消化」に留まらない、攻撃者のエクスプロイト手法を逆算したIaCセキュリティスキャンの本質、そしてCI/CDパイプラインに組み込むべき「ガードレイル」のアーキテクチャ設計について、泥臭い実践知を交えて深く掘り下げる。
—
1. 攻撃者の視点:IaCの静的欠陥がもたらす「ランタイムの破滅」
なぜ、静的解析でIaCの不備を検出せねばならないのか。それは、クラウドにおける単一の設定ミスが、単なる「情報漏洩」に留まらず、「制御権の完全奪取」へと直結するからだ。
レッドチームとして組織に侵入を試みる際、我々が最も好む脆弱性チェーン(Vulnerability Chain)の一つが、「SSRF(Server-Side Request Forgery) × IMDSv1(Instance Metadata Service v1) × 過剰権限IAMロール」の組み合わせである。
脆弱性の連鎖モデル
[攻撃者]
│
▼ (1) Webアプリケーションの脆弱性を悪用 (SSRF)
[EC2 インスタンス] (SSRFによりローカルループバック経由でIMDSv1にアクセス)
│
▼ (2) メタデータエンドポイントから一時的なIAM認証情報を奪取
[AWS API / IAM] (奪取した認証情報を使用)
│
▼ (3) 過剰権限 (IAM Policy: "*") を利用して他のリソース(S3等)を掌握
[ターゲットデータ (S3等)]
1. SSRFの実行: Webアプリケーションの脆弱性を突き、サーバー内部から任意のHTTPリクエストを送信させる。
2. メタデータの奪取: ターゲットがEC2の場合、リンクローカルアドレス http://169.254.169.254/latest/meta-data/iam/security-credentials/ へリクエストを投げさせる。ここでホストがIMDSv1を使用している場合、追加の認証(トークン)なしで、そのEC2インスタンスに付与されたIAMロールの一時的なアクセスキー、シークレットキー、セッショントークンがプレーンテキストで攻撃者の手元に渡る。
3. 特権昇格と永続化: 奪取したIAMロールに、IaCの記述ミス(例: Resource: "*" や Action: "*")によって不必要な特権が付与されていた場合、攻撃者はその権限を用いてS3バケットのデータを窃取し、最悪の場合は管理者権限を奪取してインフラ全体を掌握する。
TerraformでEC2を定義する際、デフォルトのまま、あるいは古いテンプレートをコピペして以下のように定義してしまうことが、この破滅的なシナリオの起点となる。
# 脆弱なTerraformコードの例:IMDSv2が強制されておらず、過剰なセキュリティグループを持つ
resource "aws_instance" "vulnerable_ec2" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
# 盲点:metadata_options を明示しない場合、デフォルトで脆弱なIMDSv1が有効化される
# metadata_options {
# http_tokens = "required" # IMDSv2を強制するための設定
# }
vpc_security_group_ids = [aws_security_group.wide_open.id]
}
resource "aws_security_group" "wide_open" {
name = "wide-open-sg"
description = "Insecure Security Group"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # インターネット全体からのSSHを許可(総当たり攻撃の標的)
}
}
このコードの問題は、コンパイルエラーにはならない点だ。Terraformとしては「文法的に正しい」ため、そのまま terraform apply が通り、本番環境にセキュリティホールが誕生する。これをデプロイ前に「構造的に」阻止するのが、IaCセキュリティスキャンの役割である。
—
2. ガードレイル・アーキテクチャ:ポリシー・アズ・コードの設計
IaCセキュリティを機能させるには、単に開発者のローカルPCでスキャンを実行させるだけでは不十分だ。人間は必ず「急ぎのデプロイ」や「手元のスキャンのバイパス」を行う。
目指すべきは、「チェックポイント型ガードレイル」の構築である。
[開発者] -> [Git Commit] -> [GitHub Pull Request]
│
▼ (自動トリガー)
[GitHub Actions ランナー]
├── 1. Linter (TFLint)
├── 2. 脆弱性スキャン (Trivy / Checkov)
└── 3. ポリシー検査 (OPA / Conftest)
│
┌──────────────┴──────────────┐
[違反あり (Fail)] [ポリシー適合 (Pass)]
│ │
▼ (マージブロック) ▼ (自動マージ or レビュー可能)
[開発者へフィードバック] [terraform apply / デプロイ]
アーキテクチャ設計の3つのレイヤー
1. 構文・静的解析レイヤー(Linter / Static Analysis):
tflint や terraform validate を用いて、構文のエラーやクラウドプロバイダのベストプラクティス違反(非推奨パラメーターの使用など)を高速にフィルタリングする。
2. セキュリティ・スキャニングレイヤー(Security Scanner):
Trivy、Checkov、tfsec などのオープンソースツール(OSS)を用いて、一般的な脆弱性や既知の設定ミス(S3バケットの公開設定、暗号化の未有効化など)をシグネチャベースで網羅的に検出する。
3. カスタム・ポリシー・レイヤー(Policy as Code):
組織固有のコンプライアンス要件(例:「本番環境のすべてのリソースには Env: Production タグを付与しなければならない」「特定のIP帯以外からのインバウンドルールは一律禁止」など)を、Rego(Open Policy Agent)などの言語を用いてコード化し、検証する。
—
3. 実践:カスタムRegoポリシーによる厳格なインフラ統制
ここでは、クラウドセキュリティの事実上の標準となりつつある OPA(Open Policy Agent) と、そのラッパーである Conftest を用いて、Terraformのプランファイル(plan.json)をスキャンする実用的なカスタムポリシーを実装する。
ターゲットは、先ほど挙げた「S3バケットのパブリックアクセスブロックの欠落」および「EC2におけるIMDSv2(セキュアなメタデータサービス)の強制化」である。
3.1. 検証対象となるTerraformコード(main.tf)
まずは、あえてセキュリティ上の不備を残したTerraformコードを用意する。
provider "aws" {
region = "ap-northeast-1"
}
# 1. 脆弱なS3バケット(パブリックアクセスブロックの設定がない)
resource "aws_s3_bucket" "leaky_bucket" {
bucket = "my-highly-confidential-data-bucket"
}
# 2. 脆弱なEC2(IMDSv2が必須になっていない)
resource "aws_instance" "insecure_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
# metadata_options ブロック自体が存在しない
}
このコードをそのまま適用させないために、Terraformの実行計画(Plan)をJSON形式で出力し、ポリシーエンジンに通す。
# Terraformプランをバイナリ出力
terraform plan -out=tfplan.binary
# バイナリをJSONに変換(OPAやConftestが解釈できるようにする)
terraform show -json tfplan.binary > tfplan.json
3.2. カスタムRegoポリシー(policy/infra_protection.rego)
次に、出力された tfplan.json を静的解析し、セキュリティ基準を満たさない場合にビルドを強制終了させるRegoポリシーを記述する。
package main
import rego.v1
# 許容しないリソース変更の収集
deny[msg] {
# JSONからすべてのリソース変更要素を走査
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
# 同一プラン内に、該当S3バケットに対する aws_s3_bucket_public_access_block が存在するか検証
bucket_name := resource.name
not has_public_access_block(bucket_name)
msg := sprintf("セキュリティ侵害: S3バケット '%v' に対する 'aws_s3_bucket_public_access_block' が定義されていません。パブリックアクセスは明示的にブロックする必要があります。", [bucket_name])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
# EC2のメタデータオプションを検証
metadata_options := resource.change.after.metadata_options[_]
# IMDSv2 (http_tokens) が "required" に設定されているかチェック
metadata_options.http_tokens != "required"
msg := sprintf("セキュリティ侵害: EC2インスタンス '%v' で IMDSv2 が強制されていません (http_tokens を 'required' に設定してください)。IMDSv1の利用はSSRFによる認証情報窃取のリスクを高めます。", [resource.name])
}
# ヘルパー関数: S3バケットに対するパブリックアクセスブロック設定の有無を判定
has_public_access_block(bucket_name) {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
# ターゲットバケットへの参照、またはバケット名の一致を確認
# (ここでは簡略化のため、リソースの命名規則または直接のバケット参照をマッピング)
resource.change.after.bucket == bucket_name
}
has_public_access_block(bucket_name) {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
# 参照関係(references)からバケット名が一致するかを判定
contains(resource.change.after.bucket, bucket_name)
}
3.3. Conftestによるポリシーテストの実行
作成したポリシーを使い、ローカルまたはCI環境で以下のコマンドを実行する。
# Conftestによるポリシーの検証実行
conftest test tfplan.json -p policy/
出力結果(イメージ):
FAIL - tfplan.json - main - セキュリティ侵害: S3バケット 'leaky_bucket' に対する 'aws_s3_bucket_public_access_block' が定義されていません。パブリックアクセスは明示的にブロックする必要があります。
FAIL - tfplan.json - main - セキュリティ侵害: EC2インスタンス 'insecure_server' で IMDSv2 が強制されていません (http_tokens を 'required' に設定してください)。IMDSv1の利用はSSRFによる認証情報窃取のリスクを高めます。
2 tests, 0 passed, 0 warnings, 2 failures found.
このように、開発者がコードをコミットした段階で、明確な「なぜこれが脆弱なのか」というコンテキストと共に修正を促すことができる。
—
4. セキュアに修正されたIaCコード
ポリシーをパスするために、Terraformコードを以下のように修正する。これが、本番環境にデプロイされるべき「セキュアなベースライン」である。
provider "aws" {
region = "ap-northeast-1"
}
# 1. セキュアなS3バケットの定義
resource "aws_s3_bucket" "secure_bucket" {
bucket = "my-highly-confidential-data-bucket"
}
# パブリックアクセスを厳格にブロックするリソースを明示的に紐付ける
resource "aws_s3_bucket_public_access_block" "secure_bucket_block" {
bucket = aws_s3_bucket.secure_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# 2. セキュアなEC2の定義 (IMDSv2を強制)
resource "aws_instance" "secure_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
# IMDSv2を強制し、ホップ制限を適切に設定する
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2のトークン要求を必須化
http_put_response_hop_limit = 1 # コンテナ環境等でない限り、ホップ数は1に制限してSSRF転送を防止
}
}
—
5. 次世代の盲点:LLM(生成AI)がもたらすIaCの罠と「AIガードレイル」
現代のテックリードやアーキテクトが直面している、極めて現代的な脅威がある。それは「生成AI(LLM)による脆弱なIaCの自動生成」だ。
GitHub CopilotやChatGPTは、極めて流暢にTerraformやCloudFormationのコードを書き出す。しかし、彼らが学習したソースコードの大部分は、インターネット上に転がっている「動くが、セキュリティを考慮していない」野良コードや古いドキュメントである。
LLMはしばしば、以下のような「静かなる脆弱性」をコードに忍び込ませる。
1. 廃止された古いAPIやリソースの利用: 脆弱性が既知となっている古い暗号スイートやプロトコル(TLS 1.0/1.1など)をデフォルトで使用するコードを吐き出す。
2. ハルシネーションによるセキュリティ設定の無視: 存在しない架空のセキュリティパラメータ(例:enable_super_secure_shield = true)を「もっともらしく」記述し、実際の構文解析では無視されるが、開発者は「セキュアに設定した」と錯覚する。
3. 過剰な権限の自動付与: 動かないコードを避けるため、LLMは往々にして *(ワイルドカード)を多用したIAMポリシーを提案する。
生成AI時代におけるガードレイルのアップデート
これに対抗するには、AIの利用を禁止するのではなく、「AIが生成したコードは、常に悪意あるサードパーティが書いたコードと同等に扱う(ゼロトラスト・コード)」という前提に立ち、CI/CDでの静的解析をより厳格化することだ。
具体的には、LLMが介在する開発フローにおいて、以下の二重の防衛層(デュアル・ガードレイル)を配置する。
[開発者のプロンプト入力]
│
▼
[1. 入力ガードレイル] (システムプロンプトによる制約:「セキュリティベストプラクティスを遵守せよ」)
│
▼
[LLMのコード生成]
│
▼
[2. 出力ガードレイル] (開発環境にコミットされる前のプレ・コミットフックでの自動スキャン)
│
▼
[CI/CD パイプライン] (OPA/Conftestによる最終的なデプロイブロック)
この「出力ガードレイル」として機能するのが、ローカルでの pre-commit フックに統合された Trivy や checkov である。開発者がコードを手元でGitにコミットした瞬間にスキャンを強制し、不合格なコードはリモートリポジトリにプッシュすらさせない仕組みを構築する。
—
6. 結論:真のガバナンスとしてのIaCセキュリティ
インフラの構築が「ソフトウェア開発」と同義になった今、セキュリティもまた「ソフトウェア品質」の一部として扱われなければならない。
チェックリストを印刷し、エクセルにキャプチャを貼り付けて監査役に提出するような古典的なセキュリティ統制は、近代的なクラウドネイティブの速度感の前には無力であり、開発者からの敵意を買うだけだ。
本質的なガバナンスとは、開発者が意識せずとも、自動化されたパイプラインという「見えない防壁(ガードレイル)」の内側でしかコードを書けない状態を作ることである。コードでインフラを定義するなら、その防壁もまた、コード(Policy as Code)で定義されるべきなのだ。
あなたの組織のIaCは、攻撃者にとっての「脆弱性の量産工場」になっていないだろうか。今一度、CI/CDパイプラインに組み込まれたスキャナーの「眼光」の鋭さを、見直してみてほしい。
コメント