EC2セキュリティ — IMDSv2・Nitro Enclaves・Image Builder
IMDSv2によるSSRF対策、Nitro Enclaves、EC2 Instance Connect、AMI暗号化を徹底解説
はじめに
EC2セキュリティはAWS SCS-C03試験の要であり、特にインスタンスメタデータサービス(IMDS)、キーペア管理、Nitro Enclaves、AMI暗号化の4領域が重要です。
本講では、IMDSv1の脆弱性からIMDSv2への移行、SSRF攻撃対策、Nitro Enclavesの暗号化処理、そしてEC2 Image Builderによるセキュアなイメージ構築まで、試験を突破し本番運用に耐えるセキュリティ実装を解説します。
IMDSv1 vs v2 SSRF 攻撃対策
Loading diagram...
EC2 Image Builder パイプライン(セキュアなAMI構築)
Loading diagram...
1. インスタンスメタデータサービス(IMDS)
1.1 IMDSとは
EC2インスタンスは起動時に自動的にメタデータサーバー(169.254.169.254:80)にアクセスでき、以下を取得できます:
- インスタンスID・タイプ・AZ
- IAMロールの一時的認証情報(AccessKey、SecretKey、Token)
- ネットワークインターフェース情報
- ユーザーデータ
- SSH公開鍵
メタデータへのアクセス経路:
# コマンドラインからのアクセス例
curl http://169.254.169.254/latest/meta-data/
# IAMロールの認証情報取得(危険)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
# レスポンス例
{
"Code" : "Success",
"LastUpdated" : "2026-04-27T10:30:00Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "AKIA...",
"SecretAccessKey" : "...",
"Token" : "...",
"Expiration" : "2026-04-27T11:30:00Z"
}
1.2 IMDSv1の脆弱性(SSRF攻撃ベクトル)
IMDSv1はHTTP GETリクエストを無条件に受け入れ、認証なしにメタデータを返すという設計上の欠陥があります。
SSRF(Server-Side Request Forgery)攻撃パターン:
# インスタンス内の脆弱なWebアプリが任意のURLにリクエストを送信可能な場合
# 攻撃者は以下をリクエストさせることで認証情報を盗むことができる
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
# また、インスタンスのローカルプロセスがメタデータに直接アクセスして
# 意図せず認証情報を漏洩することもある
攻撃シナリオ:
-
SSRF脆弱性を持つアプリを入れてしまった → 攻撃者が「任意のURLをfetch」する処理をトリガー → メタデータからIAM認証情報を盗用 → AWS APIへの不正アクセス
-
コンテナブレイクアウト → コンテナ内の攻撃者がホストのメタデータにアクセス → インスタンスロールの認証情報を奪取
1.3 IMDSv2の仕組み(トークン必須化)
IMDSv2では2段階認証が必須になり、SSRF耐性が大幅に向上します。
| 項目 | IMDSv1 | IMDSv2 |
|---|---|---|
| 認証方式 | 認証なし(オープン) | トークン方式(PUT→GET) |
| リクエスト方法 | 直接GET | PUTでトークン取得→GETで使用 |
| デフォルト | v1のみ有効 | v2推奨・v1廃止予定 |
| TTL(トークン有効期間) | N/A | 21秒(設定可能1-21秒) |
| ホップリミット | N/A | 1(デフォルト・設定可能) |
| SSRF対策 | ✗ なし | ✓ あり(トークン取得に手順) |
IMDSv2の フロー:
# ステップ 1: トークン取得(PUT)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21")
# ステップ 2: トークンを使ってメタデータ取得(GET)
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
# IMDSv1の方式(v2対応前の古いコード)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
SSRF攻撃時の動作の違い:
# IMDSv1: 直接リクエスト1行で認証情報が返される
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
# IMDSv2: トークン取得にはPUTメソッドが必要
# → SSRF脆弱性がGETのみしか許さない場合、トークンが取得できない
# → メタデータへのアクセスが失敗する
1.4 IMDSv2の強制(HttpTokens: required)
ベストプラクティスは全インスタンスでIMDSv2を強制することです。
インスタンスレベルでの設定:
# AWS CLIで既存インスタンスを修正
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1 \
--region us-east-1
# レスポンス確認
aws ec2 describe-instances \
--instance-ids i-1234567890abcdef0 \
--query 'Reservations[0].Instances[0].MetadataOptions'
# 出力例
{
"State": "pending",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled"
}
CloudFormationでの定義:
AWSTemplateFormatVersion: "2010-09-09"
Resources:
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t3.micro
MetadataOptions:
HttpTokens: required # IMDSv2 強制
HttpPutResponseHopLimit: 1 # ホップリミット1に設定
HttpEndpoint: enabled # メタデータエンドポイント有効
IamInstanceProfile: MyInstanceProfile
Terraformでの定義:
resource "aws_instance" "secure" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2強制
http_put_response_hop_limit = 1
}
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
}
アカウントレベルでの一括設定:
# AWS Account 全体のデフォルトをIMDSv2に設定
aws ec2 modify-instance-metadata-defaults \
--http-tokens required \
--http-put-response-hop-limit 1 \
--region us-east-1
# 設定確認
aws ec2 get-instance-metadata-defaults --region us-east-1
1.5 SSRF攻撃対策としてのIMDSv2の重要性
なぜトークン方式でSSRF対策になるのか:
-
トークン取得(PUT)とメタデータ取得(GET)が分離される
- 単純なGET SSRFではメタデータにアクセスできない
-
ホップリミット(HTTP_PUT_RESPONSE_HOP_LIMIT)で防御層を追加
- デフォルト1 → インスタンスローカルのプロセスからのみアクセス可能
- コンテナやVM内の攻撃者がホストメタデータにアクセスする難度が上がる
実装例:IAM認証情報取得の安全化
# IMDSv2対応SDK(AWS SDK v3)は自動的にv2を使用
import boto3
from botocore.compat import requests
# 新しいコード(推奨)
session = boto3.Session()
sts = session.client('sts')
caller = sts.get_caller_identity()
print(f"Account: {caller['Account']}") # メタデータから自動取得
# 古いコード(v1依存)- こちらは避ける
# 直接http://169.254.169.254にアクセスする実装は危険
2. キーペア管理と認証方式の選択肢
2.1 従来のSSHキーペアの問題点
従来のEC2キーペア(.pem/.ppk)には以下の問題があります:
-
キーローテーションが複雑
- 新キーペアに置き換える際、全インスタンスの authorized_keys を手動更新
- ダウンタイムが発生する可能性
-
キーの紛失・漏洩リスク
- ローカルに保存されたキーが盗まれると、無限にアクセス可能
- アクセス権限を細かく制御できない
-
監査ログが不十分
- 誰がいつアクセスしたか、AWSのネイティブログで追跡しにくい
2.2 EC2 Instance Connect(短命キー方式)
EC2 Instance Connect は IAM権限をベースに一時的なSSH公開鍵を自動生成する方式です。
特徴:
| 項目 | 説明 |
|---|---|
| 認証ベース | IAM権限(EC2InstanceConnect) |
| キーの有効期限 | 60秒(固定・変更不可) |
| トランスポート | SSH over HTTPS(443番) |
| ホストキー検証 | EC2InstanceConnect APIがホスト鍵を検証 |
| 監査ログ | CloudTrail(API呼び出し)+syslog(接続ログ) |
| 対応OS | EC2 Instances(AL2、Ubuntu 16.04+) |
使用方法:
# 前提条件1: インスタンスにIAMロール付与
# ロールには "ec2-instance-connect:SendSSHPublicKey" が必要
# 前提条件2: Security Group で HTTPS (443) と SSH (22) を許可
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
# EC2 Instance Connect で接続
aws ec2-instance-connect send-ssh-public-key \
--instance-id i-1234567890abcdef0 \
--instance-os-user ec2-user \
--ssh-public-key file://~/.ssh/my-key.pub \
--availability-zone us-east-1a
# その後、通常のSSH接続
ssh -i ~/.ssh/my-key ec2-user@<instance-public-ip>
CloudFormationでの権限設定:
MyEC2ConnectRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEC2InstanceConnectRoleForEC2Instance
MyInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref MyEC2ConnectRole
MyInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0885b1f6bd170450c # Ubuntu or AL2
InstanceType: t3.micro
IamInstanceProfile: !Ref MyInstanceProfile
SecurityGroupIds:
- !Ref SSHSecurityGroup
SSHSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow SSH
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
セキュリティ上の利点:
# CloudTrailログで接続を追跡
import boto3
import json
logs = boto3.client('logs')
# ログクエリ例:EC2InstanceConnect使用履歴
response = logs.start_query(
logGroupName='/aws/cloudtrail/my-trail',
startTime=int((datetime.now() - timedelta(days=1)).timestamp()),
endTime=int(datetime.now().timestamp()),
queryString='''
fields @timestamp, userIdentity.principalId, eventName, requestParameters
| filter eventName = "SendSSHPublicKey"
| stats count() by userIdentity.principalId
'''
)
2.3 Systems Manager Session Manager
Session Manager はSSHキーを完全に排除し、IAMのみで接続を制御します。
特徴:
| 項目 | 説明 |
|---|---|
| 認証ベース | IAM権限(ssm:StartSession) |
| キーレス | SSH秘密鍵が一切不要 |
| トランスポート | HTTPS(Systems Manager Endpoint経由) |
| 監査ログ | CloudTrail + Session Manager ログ(S3) |
| リバースシェル | インスタンスがAWS APIに接続(インバウンド不要) |
| ポートフォワーディング | RDS・ElastiCacheへの直接接続も可能 |
セットアップ:
# ステップ1: インスタンスロールにポリシー追加
aws iam attach-role-policy \
--role-name MyEC2Role \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
# ステップ2: Systems Manager Agent がインスタンスで実行中か確認
# (最新のAL2/UbuntuはデフォルトでSSM Agent が動作)
# ステップ3: インスタンスのネットワーク設定確認
# - VPC Endpoint for Systems Manager が必要(プライベートインスタンス)
# またはNAT Gateway 経由でインターネット接続
# ステップ4: 接続テスト
aws ssm start-session --target i-1234567890abcdef0
CloudFormationでのセットアップ:
MyEC2Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
MyInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref MyEC2Role
MyInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0885b1f6bd170450c
InstanceType: t3.micro
IamInstanceProfile: !Ref MyInstanceProfile
# セキュリティグループ: インバウンドルール不要
SecurityGroupIds:
- !Ref NoInboundSecurityGroup
NoInboundSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: No inbound rules (Session Manager only)
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Session Manager ログの記録:
# S3へのセッションログ記録を有効化
aws ssm update-document-default-version \
--name "AWS-StartInteractiveCommand" \
--document-version "\$LATEST"
# セッション設定ファイル (prefs.json)
{
"s3BucketName": "my-session-logs-bucket",
"s3KeyPrefix": "session-logs/",
"s3EncryptionEnabled": true,
"cloudWatchLogGroupName": "/aws/ssm/session-logs",
"cloudWatchEncryptionEnabled": true,
"idleSessionTimeout": "20",
"maxSessionDuration": "60",
"kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
"runAsEnabled": false,
"runAsDefaultUser": ""
}
2.4 EC2 Instance Connect Endpoint
パブリックIPを持たないプライベートインスタンスから安全にアクセスするための仲介役です。
図解:
┌─────────────────────────────────────────────────────────┐
│ インターネット側 │
│ ┌─────────────┐ │
│ │ クライアント │ │
│ │(ローカルPC) │ │
│ └──────┬──────┘ │
└───────┼────────────────────────────────────────────────┘
│ SSH/HTTPS (443)
│
┌───────┼────────────────────────────────────────────────┐
│ AWS VPC │
│ ┌───────▼───────────────────────────────────────────┐ │
│ │ EC2 Instance Connect Endpoint │ │
│ │ (パブリックサブネット) │ │
│ └──────────┬──────────────────────────────────────┘ │
│ │ SSH (22) / RDP (3389) │
│ ┌──────────▼──────────────────────────────────────┐ │
│ │ Private EC2 インスタンス │ │
│ │ (プライベートサブネット) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
セットアップ:
# ステップ1: EC2 Instance Connect Endpoint を作成
aws ec2 create-instance-connect-endpoint \
--subnet-id subnet-0123456789abcdef0 \
--security-group-ids sg-xxxxxxxxx \
--tag-specifications 'ResourceType=instance-connect-endpoint,Tags=[{Key=Name,Value=MyEndpoint}]'
# ステップ2: Endpoint IDを取得
ENDPOINT_ID=$(aws ec2 describe-instance-connect-endpoints \
--filters Name=tag:Name,Values=MyEndpoint \
--query 'InstanceConnectEndpoints[0].InstanceConnectEndpointId' \
--output text)
# ステップ3: プライベートインスタンスに接続
aws ec2-instance-connect open-tunnel \
--instance-connect-endpoint-id $ENDPOINT_ID \
--instance-id i-0987654321fedcba0 \
--local-port 8888
# ステップ4: 別ターミナルでSSH接続
ssh -i my-key.pem -p 8888 ec2-user@localhost
3. Nitro Enclaves(機密コンピューティング環境)
3.1 Nitro Enclaveとは
Nitro Enclave は EC2インスタンス内で分離されたコンピューティング環境を作り、メモリ・CPU・ストレージが暗号化・隔離される仕組みです。
ユースケース:
-
PII(個人識別情報)処理 → 顧客の機密データを安全に処理
-
暗号化キー管理 → KMSキーをEnclave内でのみ使用、外に漏らさない
-
マルチパーティ計算 → 複数企業のデータを結合分析、各企業のデータは秘密のまま
-
決済処理 → クレジットカード情報の復号・処理をEnclave内で完結
アーキテクチャ:
3.2 Attestation(証明書検証)
Attestation は「このEnclave が正しい構成で動作している」ことを証明するメカニズムです。
流れ:
- Enclave内のアプリケーションが起動時にAttestation Documentを生成
- KMS or カスタムサービスがドキュメントを検証
- 検証成功時のみ、Enclaveに対してデータ/キーを提供
CloudFormationでのEnclave起動:
MyEnclaveInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0 # Nitro対応イメージ
InstanceType: m5.large
EnclaveOptions:
Enabled: true # Nitro Enclave 有効化
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: 30
VolumeType: gp3
Encrypted: true
KmsKeyId: !GetAtt MyKMSKey.Arn
Attestation Documentの検証例(Python):
import json
import base64
import requests
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# Enclave内のアプリケーション側: Attestation Document 生成
def get_attestation_document():
"""
Nitro Enclave内のprocfs経由でAttestation Documentを取得
"""
with open('/dev/attestation/attestation.crt', 'rb') as f:
attestation_doc = f.read()
return attestation_doc
# Parent インスタンス or 外部サービス側: 検証
def verify_attestation(attestation_doc, pcr_values):
"""
Attestation Document を検証
Args:
attestation_doc: Enclave が生成した証明書
pcr_values: 期待される PCR (Platform Configuration Register) 値
Returns:
bool: 検証成功か否か
"""
# 証明書をデコード
cert = x509.load_pem_x509_certificate(
attestation_doc,
default_backend()
)
# ドキュメントの署名を検証(AWS Nitro親鍵で検証)
# PCR値(ファームウェア・Enclaveイメージのハッシュ)が一致するか確認
doc_data = json.loads(cert.public_bytes(encoding=x509.Encoding.PEM))
for i, expected_pcr in enumerate(pcr_values):
actual_pcr = doc_data['document']['pcr'][i]
if actual_pcr != expected_pcr:
return False
return True
3.3 KMS 連携(Enclave内でのみ復号)
KMS と Enclave を組み合わせることで、Enclave内でのみデータを復号可能にできます。
シナリオ:
1. ユーザーがクレジットカード情報を提出
2. 親インスタンスがKMSで暗号化して保存
3. Enclaveがユーザー認証を実施
4. Enclave がKMSにリクエスト「このEnclaveが正当か検証してくれ」
5. KMS が Attestation を確認し、特別なポリシー付きキーをEnclave内でのみ復号可能な形で返す
6. Enclave がカード情報を復号・処理
7. 処理後、親インスタンスには復号済みデータが戻らない
KMS キーポリシーの例:
{
"Sid": "AllowNitroEnclaveOnly",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyEC2Role"
},
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:RecipientAttestation:ImageSha384": "aaaabbbbccccdddd..."
}
}
}
KMS DecryptWithEnclave呼び出し(Enclave内):
import boto3
import json
kms = boto3.client('kms')
def decrypt_in_enclave(encrypted_data, pcr_claim):
"""
Enclave 内での復号(Attestation検証付き)
"""
response = kms.decrypt(
CiphertextBlob=encrypted_data,
EncryptionContext={
'pcr': pcr_claim # Enclave の PCR値を証明
}
)
return response['Plaintext']
3.4 Nitro Enclave の実装例:機密データ処理
# Parent インスタンス側 (privileged=true)
import socket
import struct
import json
class EnclaveClient:
def __init__(self, enclave_cid, enclave_port):
self.enclave_cid = enclave_cid
self.enclave_port = enclave_port
def send_request(self, data):
"""
Enclave へリクエスト送信(vsock通信)
"""
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.connect((self.enclave_cid, self.enclave_port))
message = json.dumps(data).encode()
sock.sendall(struct.pack('<I', len(message)) + message)
# レスポンス受信
response_len_data = sock.recv(4)
response_len = struct.unpack('<I', response_len_data)[0]
response_data = sock.recv(response_len)
sock.close()
return json.loads(response_data.decode())
# Enclave側(Enclave内で実行)
import socket
import json
import struct
from cryptography.fernet import Fernet
class EnclaveServer:
def __init__(self, port=5000):
self.port = port
self.encryption_key = Fernet.generate_key()
self.cipher = Fernet(self.encryption_key)
def start(self):
"""
vsock 上でサーバー起動
"""
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.bind((socket.VMADDR_CID_ANY, self.port))
sock.listen(1)
print(f"Enclave listening on port {self.port}")
while True:
conn, addr = sock.accept()
self.handle_request(conn)
def handle_request(self, conn):
"""
Enclave 内での処理(外部からは見えない)
"""
# リクエスト受信
msg_len_data = conn.recv(4)
msg_len = struct.unpack('<I', msg_len_data)[0]
msg_data = conn.recv(msg_len)
request = json.loads(msg_data.decode())
# 例: クレジットカード情報を復号・検証
if request['action'] == 'decrypt_card':
encrypted_card = request['encrypted_card'].encode()
decrypted = self.cipher.decrypt(encrypted_card).decode()
# カード番号の検証(Luhnアルゴリズム等)
is_valid = self.validate_card(decrypted)
response = {
'status': 'success' if is_valid else 'invalid',
'card_last_four': decrypted[-4:] if is_valid else None
}
else:
response = {'status': 'error', 'message': 'Unknown action'}
# レスポンス送信
response_data = json.dumps(response).encode()
conn.sendall(struct.pack('<I', len(response_data)) + response_data)
conn.close()
@staticmethod
def validate_card(card_number):
"""Luhnアルゴリズムでカード番号検証"""
digits = [int(d) for d in card_number if d.isdigit()]
checksum = 0
for i, digit in enumerate(reversed(digits)):
if i % 2 == 1:
digit *= 2
if digit > 9:
digit -= 9
checksum += digit
return checksum % 10 == 0
# 実行例
if __name__ == '__main__':
# Enclave側
server = EnclaveServer()
server.start() # Enclave内で起動
# Parent側
client = EnclaveClient(enclave_cid=16, enclave_port=5000)
response = client.send_request({
'action': 'decrypt_card',
'encrypted_card': 'gAAAAABl...' # 暗号化されたカード情報
})
print(response)
4. セキュリティグループ・ネットワーク・ストレージ暗号化
4.1 インスタンスプロファイル(IAMロール付与)
EC2 インスタンスが AWS APIを呼び出す際には、インスタンスプロファイル経由で IAMロールを付与します。
構造:
┌─────────────────────────────────────┐
│ EC2 インスタンス │
│ │
│ ┌──────────────────────────────┐ │
│ │ Instance Metadata Service │ │
│ │ - ロール名 │ │
│ │ - 一時認証情報(AccessKey) │ │
│ │ - Token (STS) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
↓
┌───────────────────────┐
│ Instance Profile │
│ (ロールのコンテナ) │
└───────────────────────┘
↓
┌───────────────────────┐
│ IAM ロール │
│ (権限の集合) │
└───────────────────────┘
CloudFormationでの定義:
MyEC2Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: S3Access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: arn:aws:s3:::my-bucket/*
- Effect: Allow
Action:
- kms:Decrypt
- kms:GenerateDataKey
Resource: !GetAtt MyKMSKey.Arn
MyInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref MyEC2Role
MyInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t3.micro
IamInstanceProfile: !Ref MyInstanceProfile
4.2 EBS ボリューム暗号化
EC2 のブロックストレージ(EBS)はデフォルトで暗号化設定されるべきです。
| 項目 | 説明 |
|---|---|
| デフォルト暗号化 | リージョンレベルで有効化可能 |
| KMS キー | AWS管理キー or カスタマー管理キー |
| スナップショット | 暗号化ボリュームのスナップショットも暗号化 |
| スナップショット共有 | カスタマー管理キーで共有時、ターゲットアカウントにキーアクセス許可が必須 |
デフォルト暗号化の有効化:
# リージョンレベルでデフォルト暗号化を有効化
aws ec2 enable-ebs-encryption-by-default \
--region us-east-1
# 確認
aws ec2 get-ebs-encryption-by-default --region us-east-1
CloudFormationでのEBS定義:
MyInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t3.micro
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: 30
VolumeType: gp3
Iops: 3000
Throughput: 125
Encrypted: true # 暗号化を明示的に指定
KmsKeyId: !GetAtt MyKMSKey.Arn # カスタマー管理キーを指定
DeleteOnTermination: true
MyKMSKey:
Type: AWS::KMS::Key
Properties:
Description: "KMS key for EBS encryption"
KeyPolicy:
Version: "2012-10-17"
Statement:
- Sid: Enable IAM policies
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
- Sid: Allow EBS service
Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action:
- kms:Decrypt
- kms:GenerateDataKey
- kms:CreateGrant
Resource: "*"
4.3 AMI の暗号化とクロスアカウント共有
AMIを別アカウントと共有する場合、暗号化キーのアクセス許可が必須です。
シナリオ:
Account A (AMI所有)
├─ AMI (encrypted with KMS key A)
└─ KMS Key A
↓ AMI共有リクエスト
Account B (受け取り側)
├─ スナップショットコピー
└─ KMS Key B でコピーを再暗号化
セットアップ:
# Account A: AMI作成・確認
aws ec2 create-image \
--instance-id i-1234567890abcdef0 \
--name "MySecureAMI" \
--description "Encrypted AMI for cross-account sharing" \
--region us-east-1
IMAGE_ID=$(aws ec2 describe-images \
--filters Name=name,Values=MySecureAMI \
--query 'Images[0].ImageId' \
--output text)
# Account A: KMS キーポリシー修正(Account B に許可)
# キーポリシーに以下を追加
{
"Sid": "AllowAccountBDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT-B-ID:root"
},
"Action": [
"kms:Decrypt",
"kms:DescribeKey",
"kms:GenerateDataKey"
],
"Resource": "*"
}
# Account A: AMI を Account B と共有
aws ec2 modify-image-attribute \
--image-id $IMAGE_ID \
--launch-permission Add={UserId=ACCOUNT-B-ID} \
--region us-east-1
# Account B: 共有 AMI を確認・コピー
aws ec2 describe-images \
--owners ACCOUNT-A-ID \
--region us-east-1
# Account B: AMI をコピー(自アカウントの KMS キーで再暗号化)
aws ec2 copy-image \
--source-image-id $IMAGE_ID \
--source-region us-east-1 \
--name "MyAMI-AccountB" \
--encrypted \
--kms-key-id arn:aws:kms:us-east-1:ACCOUNT-B-ID:key/KEY-ID \
--region us-east-1
4.4 Dedicated Hosts / Dedicated Instances
コンプライアンス要件(物理サーバーの占有、ライセンス管理)により、Dedicated Host / Dedicated Instance が必要な場合があります。
| 項目 | Shared | Dedicated Instance | Dedicated Host |
|---|---|---|---|
| 物理ホスト | 複数顧客共有 | 単一顧客専有 | 単一顧客専有 |
| ライセンス持ち込み(BYOL) | ✗ | ✗ | ✓ |
| 物理属性制御 | ✗ | ✗ | ✓ |
| コスト | 最安 | 中程度 | 高額(ホスト単位) |
| スナップショット | 制限なし | 制限なし | ホスト内のみ |
CloudFormationでの定義:
# Dedicated Instance
MyDedicatedInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: m5.large
Tenancy: dedicated # Dedicated Instance に設定
SubnetId: !Ref MySubnet
# Dedicated Host
MyDedicatedHost:
Type: AWS::EC2::Host
Properties:
AvailabilityZone: us-east-1a
InstanceType: m5.large
Quantity: 1
MyHostInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: m5.large
Tenancy: host
HostId: !Ref MyDedicatedHost
5. EC2 Image Builder(セキュアなAMI構築)
5.1 EC2 Image Builder の概要
EC2 Image Builder は、セキュリティパッチ・ハードニング・コンプライアンステストを自動化してAMIを構築するサービスです。
パイプライン:
┌───────────────────────────────────┐
│ 1. Base Image (Amazon Linux 2) │
└───────────────┬───────────────────┘
↓
┌───────────────────────────────────┐
│ 2. Components (Build Steps) │
│ - Package Update │
│ - Security Hardening │
│ - Custom Software Install │
└───────────────┬───────────────────┘
↓
┌───────────────────────────────────┐
│ 3. Test Phase │
│ - Inspector (Vulnerability Scan) │
│ - Custom Tests │
└───────────────┬───────────────────┘
↓
┌───────────────────────────────────┐
│ 4. Output AMI (Encrypted) │
│ - Versioning │
│ - Distribution (S3/ECR) │
└───────────────────────────────────┘
5.2 CloudFormation による Image Builder 構築
AWSTemplateFormatVersion: "2010-09-09"
Description: "Secure AMI Pipeline with EC2 Image Builder"
Resources:
# IAM Role for Image Builder
ImageBuilderRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder
Policies:
- PolicyName: S3Access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub "arn:aws:s3:::${ArtifactBucket}/*"
ImageBuilderInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref ImageBuilderRole
# Artifact Bucket
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
# Image Builder Component (Security Hardening)
HardeningComponent:
Type: AWS::ImageBuilder::Component
Properties:
Name: SecurityHardening
Platform: Linux
Version: "1.0.0"
Data: |
name: Security Hardening Component
description: CIS Benchmarks and security updates
schemaVersion: 1.0
phases:
- name: build
steps:
- name: UpdatePackages
action: ExecuteBash
inputs:
commands:
- yum update -y
- yum install -y aide
- name: ApplySecuritySettings
action: ExecuteBash
inputs:
commands:
- echo "net.ipv4.ip_forward = 0" >> /etc/sysctl.conf
- echo "net.ipv4.conf.all.send_redirects = 0" >> /etc/sysctl.conf
- sysctl -p
- name: DisableUnnecessaryServices
action: ExecuteBash
inputs:
commands:
- systemctl disable avahi-daemon
- systemctl disable cups
- name: ConfigureSSH
action: ExecuteBash
inputs:
commands:
- sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
- sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
# Image Recipe
MyImageRecipe:
Type: AWS::ImageBuilder::ImageRecipe
Properties:
Name: MySecureAMI
Version: "1.0.0"
ParentImage: !Sub "arn:aws:imagebuilder:${AWS::Region}:aws:image/amazonlinux-2-x86_64/2024.3.0"
Components:
- ComponentArn: !Sub "arn:imagebuilder:${AWS::Region}:aws:component/update-linux/1.0.2/1"
- ComponentArn: !GetAtt HardeningComponent.Arn
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
DeleteOnTermination: true
VolumeSize: 30
VolumeType: gp3
Encrypted: true
# Infrastructure Configuration
InfrastructureConfig:
Type: AWS::ImageBuilder::InfrastructureConfiguration
Properties:
Name: MyInfrastructure
InstanceProfileName: !Ref ImageBuilderInstanceProfile
InstanceTypes:
- t3.medium
Logging:
S3Logs:
S3BucketName: !Ref ArtifactBucket
S3KeyPrefix: imagebuilder-logs/
TerminateInstanceOnFailure: true
# Distribution Configuration
DistributionConfig:
Type: AWS::ImageBuilder::DistributionConfiguration
Properties:
Name: MyDistribution
Distributions:
- Region: !Ref AWS::Region
AmiDistributionConfiguration:
Name: MySecureAMI-{{ imagebuilder:buildDate }}
Description: "Hardened AMI with security patches"
AmiTags:
Environment: production
BuildDate: "{{ imagebuilder:buildDate }}"
# Image Pipeline
ImagePipeline:
Type: AWS::ImageBuilder::ImagePipeline
Properties:
Name: MySecureAMIPipeline
InfrastructureConfigurationArn: !GetAtt InfrastructureConfig.Arn
ImageRecipeArn: !GetAtt MyImageRecipe.Arn
DistributionConfigurationArn: !GetAtt DistributionConfig.Arn
EnhancedImageMetadataEnabled: true
Schedule:
ScheduleExpression: "cron(0 0 * * MON *)" # 毎週月曜に実行
Outputs:
ImagePipelineArn:
Value: !GetAtt ImagePipeline.Arn
ArtifactBucketName:
Value: !Ref ArtifactBucket
5.3 CIS Benchmark 準拠ハードニング
# CIS Benchmark コンポーネント例
CISHardeningComponent:
Type: AWS::ImageBuilder::Component
Properties:
Name: CISBenchmarkHardening
Platform: Linux
Version: "2.0.0"
Data: |
name: CIS Benchmark Hardening
description: Implements CIS Amazon Linux 2 Benchmark recommendations
schemaVersion: 1.0
phases:
- name: build
steps:
# 1.1.1.1 CIS 1.1.1.1
- name: DisableUnnecessedFilesystems
action: ExecuteBash
inputs:
commands:
- echo "install cramfs /bin/true" >> /etc/modprobe.d/disable.conf
- echo "install freevxfs /bin/true" >> /etc/modprobe.d/disable.conf
- echo "install jffs2 /bin/true" >> /etc/modprobe.d/disable.conf
- echo "install hfs /bin/true" >> /etc/modprobe.d/disable.conf
- echo "install hfsplus /bin/true" >> /etc/modprobe.d/disable.conf
# 1.1.2 CIS 1.1.2-1.1.4
- name: MountOptionsPartitions
action: ExecuteBash
inputs:
commands:
- mount -o remount,nodev /tmp
- mount -o remount,nosuid /tmp
- mount -o remount,noexec /tmp
# 3.1.1 IP Forwarding の無効化
- name: DisableIPForwarding
action: ExecuteBash
inputs:
commands:
- sysctl -w net.ipv4.ip_forward=0
- echo "net.ipv4.ip_forward = 0" >> /etc/sysctl.d/99-hardening.conf
# 4.1.1 Auditing の設定
- name: EnableAuditing
action: ExecuteBash
inputs:
commands:
- yum install -y audit
- systemctl enable auditd
- auditctl -w /etc/passwd -p wa -k identity
- auditctl -w /etc/shadow -p wa -k identity
# SSH ハードニング
- name: HardenSSHConfiguration
action: ExecuteBash
inputs:
commands:
- sed -i 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
- sed -i 's/^#HostbasedAuthentication.*/HostbasedAuthentication no/' /etc/ssh/sshd_config
- sed -i 's/^#PermitEmptyPasswords.*/PermitEmptyPasswords no/' /etc/ssh/sshd_config
- echo "Protocol 2" >> /etc/ssh/sshd_config
- systemctl restart sshd
5.4 Image Builder と Inspector の統合
# Image Builder 出力 AMI を Inspector でスキャン
aws inspector start-assessment-run \
--assessment-template-arn arn:aws:inspector:us-east-1:123456789012:template/0-XXXXXXXX \
--region us-east-1
# スキャン結果を CloudWatch Logs に送信
aws logs put-metric-alarm \
--alarm-name ImageBuilderVulnerabilities \
--alarm-description "Alert on critical vulnerabilities in Image Builder AMI" \
--metric-name VulnerabilityCount \
--namespace AWS/Inspector \
--statistic Sum \
--period 3600 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:AlertTopic
試験で狙われるポイント
⭐ IMDSv2(最重要)
-
IMDSv1 vs v2の違いを正確に説明できるか
- v1: 認証なし、GET直接アクセス → SSRF脆弱性
- v2: トークン方式、PUT→GET、ホップリミット → SSRF耐性
-
HttpTokens: required の設定方法(CLI / CloudFormation / Terraform)
-
IMDSv2がSSRF対策になる理由
- GETだけではアクセス不可、PUT(トークン取得)が必須
- ホップリミット1でホスト内部からのみアクセス可能
⭐ キーペア管理の選択肢
-
EC2 Instance Connect
- 短命キー(60秒)、IAM権限ベース
- CloudTrailで監査可能
-
Systems Manager Session Manager
- キーレス、IAM権限のみ
- プライベートインスタンスにも接続可能
-
従来のSSHキーペア
- ローテーション困難、無期限アクセス
- 新規構築では避けるべき
⭐ Nitro Enclave
-
Attestation Document の役割
- Enclave が正当か検証
- KMS キーの条件付きアクセスに使用
-
ユースケース (PII処理、暗号化キー管理)
-
vsock 通信 の理解
- Parent ↔ Enclave の通信メカニズム
⭐ EBS・AMI 暗号化
-
デフォルト暗号化の有効化(リージョンレベル)
-
クロスアカウント AMI 共有時の KMS キーポリシー
- ターゲットアカウントに復号権限が必須
-
スナップショット共有 も同様にキーポリシー修正が必要
⭐ EC2 Image Builder
-
パイプライン (Component → Recipe → Infrastructure → Distribution)
-
CIS Benchmark 準拠 ハードニングの実装
-
Inspector 統合 でスキャン自動化
試験頻出問題パターン
-
「IMDSv1 で認証情報が盗まれている。どう対策するか?」 → IMDSv2 に強制、HttpTokens: required
-
「プライベートインスタンスに接続したいが、SSH キーペアを配布したくない」 → Session Manager (キーレス)、EC2 Instance Connect Endpoint
-
「Enclave 内でのみ復号させたい」 → KMS キーポリシーで PCR 条件付け、Attestation で検証
-
「AMI をセキュアに構築し、毎週更新したい」 → Image Builder パイプライン、スケジュール実行、Inspector テスト
-
「EBS が暗号化されていない。全インスタンスを強制したい」 → リージョンレベルデフォルト暗号化、新規構築時は CloudFormation で Encrypted: true
まとめ
| 領域 | 重要な技術 | 試験での出題頻度 |
|---|---|---|
| IMDS | IMDSv2、HttpTokens | ⭐⭐⭐⭐⭐ |
| キーペア | EC2IC、Session Manager | ⭐⭐⭐⭐ |
| Nitro | Attestation、KMS連携 | ⭐⭐⭐ |
| 暗号化 | EBS/AMI、デフォルト化 | ⭐⭐⭐⭐ |
| Image Builder | パイプライン、CIS | ⭐⭐⭐ |
EC2 セキュリティは SSRF 対策(IMDSv2) と キーペア管理(キーレス化) の2本柱が最優先です。本番運用では「認証情報の盗難」「不正アクセス」を徹底的に防ぐ設計が求められます。