@shinyaz

VPC Encryption Controls を実機検証 — Monitor モードの即日導入から Enforce 移行の落とし穴まで

目次

はじめに

2025年11月21日、AWS は VPC Encryption Controls を発表した。VPC 内およびVPC 間のトラフィック暗号化状況を監査(monitor)し、暗号化を強制(enforce)できるセキュリティ機能である。詳細は AWS ブログの紹介記事を参照。2026年3月1日からは有料化され、非空 VPC あたり $0.15/h(us-east-1)の課金が発生する。

VPC Encryption Controls は monitor モードの導入コストがほぼゼロである一方、enforce モードへの移行は非同期処理で時間がかかり、失敗する場合もある。このギャップの大きさはワークロードごとに異なるため、まず monitor で現状を把握することが最も合理的な第一歩になる——これが本記事の仮説である。

本記事では monitor/enforce の両モードを実機検証し、各ステップの具体的なコマンド・出力・所要時間を示す。読者は自環境の暗号化状況を棚卸しし、enforce 移行の要否と工数を判断できるようになる。公式ドキュメントは Enforce VPC encryption in transit を参照。

前提条件:

  • AWS CLI 設定済み(EC2, VPC, IAM, CloudWatch Logs の権限)
  • 検証リージョン: us-east-1

セットアップだけ見たい場合は検証環境のセットアップ、結果だけ見たい場合は検証 1に進んでほしい。

VPC Encryption Controls の概要

VPC Encryption Controls は AWS Nitro System のハードウェアレベル暗号化と、アプリケーション層の暗号化(TLS)の両方を活用して、VPC 内の暗号化を制御する。Nitro System は AWS が設計したハードウェアとソフトウェアの組み合わせで、対応インスタンス間のトラフィックを AES-256-GCM で自動的に暗号化する。

VPC Encryption Controls は 2 つのモードで動作する。

モード動作既存 VPC への適用
MonitorFlow Logs に encryption-status フィールドを追加し、暗号化状況を可視化直接有効化可能
Enforce非暗号化トラフィックをブロックし、非対応リソースの作成を防止Monitor 経由でのみ移行可能

encryption-status は以下の値を取る:

意味
0非暗号化
1Nitro ハードウェア暗号化
2アプリケーション層暗号化(PrivateLink エンドポイント経由の TLS)
3Nitro + アプリケーション層の両方
-不明または Encryption Controls 無効

料金は VPC あたりの時間課金で、リージョンにより異なる。詳細は Monitor vs Enforce: 移行判断のポイント で考察する。

検証環境のセットアップ

VPC・サブネット・セキュリティグループ・EC2 インスタンスの作成手順

検証環境は以下の構成で構築した。

  • VPC: 10.100.0.0/16
  • プライベートサブネット: 10.100.2.0/24(us-east-1a)
  • セキュリティグループ: HTTP(80), HTTPS(443), SSH(22), ICMP を VPC CIDR に限定
  • SSM 用 VPC エンドポイント: ssm, ssmmessages, ec2messages
  • Internet Gateway(Enforce 検証で使用)

VPC・サブネット・Internet Gateway

Terminal (VPC 作成)
# VPC
VPC_ID=$(aws ec2 create-vpc \
  --cidr-block 10.100.0.0/16 \
  --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=vpc-enc-verify}]' \
  --query 'Vpc.VpcId' --output text)
 
# DNS 設定(VPC エンドポイントの Private DNS に必要)
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames '{"Value":true}'
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support '{"Value":true}'
 
# プライベートサブネット
PRIVATE_SUBNET_ID=$(aws ec2 create-subnet \
  --vpc-id $VPC_ID --cidr-block 10.100.2.0/24 \
  --availability-zone us-east-1a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=enc-verify-private}]' \
  --query 'Subnet.SubnetId' --output text)
 
# Internet Gateway(Enforce 検証で使用)
IGW_ID=$(aws ec2 create-internet-gateway \
  --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=enc-verify-igw}]' \
  --query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID

セキュリティグループ

Terminal
SG_ID=$(aws ec2 create-security-group \
  --group-name enc-verify-sg \
  --description "Allow HTTP/HTTPS/SSH for encryption verification" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)
 
aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --ip-permissions \
    '[{"IpProtocol":"tcp","FromPort":80,"ToPort":80,"IpRanges":[{"CidrIp":"10.100.0.0/16"}]},
      {"IpProtocol":"tcp","FromPort":443,"ToPort":443,"IpRanges":[{"CidrIp":"10.100.0.0/16"}]},
      {"IpProtocol":"tcp","FromPort":22,"ToPort":22,"IpRanges":[{"CidrIp":"10.100.0.0/16"}]},
      {"IpProtocol":"icmp","FromPort":-1,"ToPort":-1,"IpRanges":[{"CidrIp":"10.100.0.0/16"}]}]'

IAM ロール(SSM + Flow Logs)

Terminal (SSM 用)
# SSM ロール
aws iam create-role --role-name enc-verify-ssm-role \
  --assume-role-policy-document '{
    "Version":"2012-10-17",
    "Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]
  }'
aws iam attach-role-policy --role-name enc-verify-ssm-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
 
# インスタンスプロファイル
aws iam create-instance-profile --instance-profile-name enc-verify-ssm-profile
aws iam add-role-to-instance-profile \
  --instance-profile-name enc-verify-ssm-profile --role-name enc-verify-ssm-role
Terminal (Flow Logs 用)
aws iam create-role --role-name enc-verify-flow-logs-role \
  --assume-role-policy-document '{
    "Version":"2012-10-17",
    "Statement":[{"Effect":"Allow","Principal":{"Service":"vpc-flow-logs.amazonaws.com"},"Action":"sts:AssumeRole"}]
  }'
aws iam put-role-policy --role-name enc-verify-flow-logs-role \
  --policy-name flow-logs-publish \
  --policy-document '{
    "Version":"2012-10-17",
    "Statement":[{"Effect":"Allow","Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents","logs:DescribeLogGroups","logs:DescribeLogStreams"],"Resource":"*"}]
  }'

SSM 用 VPC エンドポイント

プライベートサブネットからインターネットアクセスなしで SSM を使うために必要。

Terminal
for SVC in ssm ssmmessages ec2messages; do
  aws ec2 create-vpc-endpoint \
    --vpc-id $VPC_ID \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.us-east-1.$SVC \
    --subnet-ids $PRIVATE_SUBNET_ID \
    --security-group-ids $SG_ID \
    --private-dns-enabled
done

EC2 インスタンスの起動

Terminal (AMI 取得)
# ARM (m7g 用)
ARM_AMI=$(aws ec2 describe-images \
  --owners amazon \
  --filters 'Name=name,Values=al2023-ami-2023.*-arm64' 'Name=state,Values=available' \
  --query 'sort_by(Images,&CreationDate)[-1].ImageId' --output text)
 
# x86 (t2 用)
X86_AMI=$(aws ec2 describe-images \
  --owners amazon \
  --filters 'Name=name,Values=al2023-ami-2023.*-x86_64' 'Name=state,Values=available' \
  --query 'sort_by(Images,&CreationDate)[-1].ImageId' --output text)
Terminal (インスタンス起動)
# Web サーバー (Nitro, m7g.medium)
aws ec2 run-instances --image-id $ARM_AMI --instance-type m7g.medium \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=enc-verify-ssm-profile \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=enc-verify-server-nitro}]'
 
# クライアント A (Nitro, m7g.medium)
aws ec2 run-instances --image-id $ARM_AMI --instance-type m7g.medium \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=enc-verify-ssm-profile \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=enc-verify-client-nitro}]'
 
# クライアント B (非 Nitro, t2.micro)
aws ec2 run-instances --image-id $X86_AMI --instance-type t2.micro \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=enc-verify-ssm-profile \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=enc-verify-client-non-nitro}]'

インスタンスが running になり SSM に登録されるまで約2〜3分待つ。aws ssm describe-instance-informationPingStatus: Online を確認する。

Web サーバーの起動

SSM 経由でサーバーに HTTP/HTTPS サーバーを起動する。

Terminal
SERVER_ID=i-XXXXXXXXX  # サーバーのインスタンス ID に置き換え
 
aws ssm send-command --instance-ids $SERVER_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=[
    "openssl req -x509 -nodes -days 1 -newkey rsa:2048 -keyout /tmp/server.key -out /tmp/server.crt -subj /CN=enc-verify 2>/dev/null",
    "cat > /tmp/https_server.py << PYEOF\nimport http.server, ssl, threading\ndef run_http():\n    s = http.server.HTTPServer((\"0.0.0.0\", 80), http.server.SimpleHTTPRequestHandler)\n    s.serve_forever()\ndef run_https():\n    s = http.server.HTTPServer((\"0.0.0.0\", 443), http.server.SimpleHTTPRequestHandler)\n    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n    ctx.load_cert_chain(\"/tmp/server.crt\", \"/tmp/server.key\")\n    s.socket = ctx.wrap_socket(s.socket, server_side=True)\n    s.serve_forever()\nthreading.Thread(target=run_http, daemon=True).start()\nrun_https()\nPYEOF",
    "nohup python3 /tmp/https_server.py > /tmp/server.log 2>&1 &",
    "sleep 2 && curl -s -o /dev/null -w %{http_code} http://localhost && curl -sk -o /dev/null -w %{http_code} https://localhost"
  ]'

出力が 200200 であれば HTTP/HTTPS 両方が動作している。

検証 1: Monitor モードで暗号化ステータスを可視化する

Encryption Controls の有効化

Terminal
aws ec2 create-vpc-encryption-control \
  --vpc-id vpc-09456d794ff62c982 \
  --tag-specifications 'ResourceType=vpc-encryption-control,Tags=[{Key=Name,Value=enc-verify-control}]'
Output
{
    "VpcEncryptionControl": {
        "VpcId": "vpc-09456d794ff62c982",
        "VpcEncryptionControlId": "vpcec-070c7cd71ca25ecc8",
        "Mode": "monitor",
        "State": "creating",
        "StateMessage": "creating"
    }
}

約1分後に Stateavailable に遷移した。既存のトラフィックへの影響はなく、即座に有効化できる。

Flow Logs の作成

encryption-status フィールドを含むカスタムフォーマットで Flow Logs を作成する。CloudWatch Logs のロググループを先に作成しておく。

Terminal
aws logs create-log-group --log-group-name /vpc/enc-verify-flow-logs
 
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids vpc-09456d794ff62c982 \
  --traffic-type ALL \
  --log-destination-type cloud-watch-logs \
  --log-group-name /vpc/enc-verify-flow-logs \
  --deliver-logs-permission-arn arn:aws:iam::381492023699:role/enc-verify-flow-logs-role \
  --log-format '${flow-direction} ${traffic-path} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${encryption-status} ${reject-reason}'

トラフィック生成と結果

4 パターンのトラフィックを生成した。

パターンクライアントプロトコル期待する encryption-status
ANitro (m7g)HTTP (80)1 (Nitro 暗号化)
BNitro (m7g)HTTPS (443)3 (Nitro + TLS)
C非 Nitro (t2)HTTP (80)0 (非暗号化)
D非 Nitro (t2)HTTPS (443)2 (TLS のみ)

SSM 経由で各クライアントから HTTP(port 80)と HTTPS(port 443)のリクエストを送信した。

Terminal (SSM 経由でトラフィック生成)
# Nitro クライアント
aws ssm send-command --instance-ids $NITRO_CLIENT_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=[
    "for i in $(seq 1 30); do curl -s -o /dev/null http://10.100.2.110; done",
    "for i in $(seq 1 30); do curl -sk -o /dev/null https://10.100.2.110; done"
  ]'
 
# 非 Nitro クライアント
aws ssm send-command --instance-ids $NON_NITRO_CLIENT_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=[
    "for i in $(seq 1 30); do curl -s -o /dev/null http://10.100.2.110; done",
    "for i in $(seq 1 30); do curl -sk -o /dev/null https://10.100.2.110; done"
  ]'

約3分後に Flow Logs に結果が反映された。以下のコマンドで確認できる。

Terminal (Flow Logs の確認)
aws logs filter-log-events \
  --log-group-name /vpc/enc-verify-flow-logs \
  --filter-pattern "10.100.2.110" \
  --limit 50 \
  --query 'events[*].message' --output text
Output (Nitro クライアント → サーバー, HTTP port 80)
egress 1 10.100.2.173 10.100.2.110 56738 80 6 1 -
egress 1 10.100.2.173 10.100.2.110 56750 80 6 1 -
egress 1 10.100.2.173 10.100.2.110 56684 80 6 1 -

各フィールドは --log-format で指定した順に対応する: flow-direction / traffic-path / srcaddr / dstaddr / srcport / dstport / protocol(6=TCP)/ encryption-status / reject-reason。末尾から2番目の 1 が encryption-status で、Nitro 暗号化を示す。最後の - は reject-reason(拒否なし)。

Output (非 Nitro クライアント → サーバー, HTTP port 80)
ingress - 10.100.2.139 10.100.2.110 45186 80 6 1 -
ingress - 10.100.2.139 10.100.2.110 45210 80 6 1 -
ingress - 10.100.2.139 10.100.2.110 45280 80 6 1 -

HTTPS(port 443)のトラフィックも、Nitro・非 Nitro ともに全て encryption-status1 だった。

予想外の結果: 全トラフィックが encryption-status 1

4 パターン全てで encryption-status1(Nitro 暗号化)だった。期待していた 023 は一切出現しなかった。

パターン期待値実測値
A: Nitro (m7g) → HTTP11
B: Nitro (m7g) → HTTPS31
C: 非 Nitro (t2) → HTTP01
D: 非 Nitro (t2) → HTTPS21

t2.micro からのトラフィックが 0 ではなく 1 だった点が特に予想外だった。AWS ブログでは t4g.medium を使って 0 が記録されている。そこで、ブログと同条件(m7g.medium + t4g.medium、HTTP のみ)で追加検証を行った。

追加検証: ブログと同条件(t4g.medium)での再テスト

別の VPC を作成し、ブログと同じインスタンスタイプの組み合わせで検証した。セットアップ手順は検証 1 と同様(VPC + サブネット + SSM エンドポイント + Encryption Controls)で、インスタンスタイプのみ変更している。

追加検証のセットアップ手順

検証 1 のセットアップと同じ手順で VPC・サブネット・SG・SSM エンドポイントを作成し、以下の2台を起動する。

Terminal
# サーバー (Nitro v4, m7g.medium)
aws ec2 run-instances --image-id $ARM_AMI --instance-type m7g.medium \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=enc-verify-ssm-profile \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=blog-server-m7g}]'
 
# クライアント (Nitro v2, t4g.medium)
aws ec2 run-instances --image-id $ARM_AMI --instance-type t4g.medium \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --iam-instance-profile Name=enc-verify-ssm-profile \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=blog-client-t4g}]'

サーバーに HTTP サーバーを起動し、両クライアントから HTTP リクエストを送信する。

Terminal
# サーバーで HTTP サーバー起動
aws ssm send-command --instance-ids $SERVER_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["nohup python3 -m http.server 80 > /tmp/http.log 2>&1 &"]'
 
# 各クライアントからトラフィック生成
aws ssm send-command --instance-ids $CLIENT_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["for i in $(seq 1 30); do curl -s -o /dev/null http://SERVER_IP; done"]'
クライアントインスタンスタイプNitro バージョンencryption-status
m7g.mediumNitro v4(暗号化対応)1
t4g.mediumNitro v2(暗号化非対応)0
Output (t4g.medium → m7g.medium サーバー, HTTP port 80)
egress 1 10.0.128.91 10.0.128.31 55906 80 6 0 -
egress 1 10.0.128.91 10.0.128.31 55772 80 6 0 -
Output (m7g.medium → m7g.medium サーバー, HTTP port 80)
egress 1 10.0.128.22 10.0.128.31 58172 80 6 1 -
egress 1 10.0.128.22 10.0.128.31 58148 80 6 1 -

ブログの結果が再現された。t4g.medium(Nitro v2)からのトラフィックは 0、m7g.medium(Nitro v4)からのトラフィックは 1 である。

知見の整理

この 2 回の検証から、以下の知見が得られた。

知見 1: encryption-status は Nitro バージョンに依存する。 Nitro v3 以降(m7g 等)は暗号化対応で 1 が記録される。Nitro v2(t4g 等)は暗号化非対応で 0 が記録される。これはドキュメントの「Nitro v3 以降が Encryption in transit をサポート」という記載と一致する。

知見 2: t2.micro(Xen ベース、非 Nitro)が 1 を返した理由は不明。 t4g(Nitro v2)が 0 を返す一方で、t2(非 Nitro)が 1 を返すのは直感に反する。get-vpc-resources-blocking-encryption-enforcement でも t2.micro はブロッキングリソースとして検出されなかった。正確な原因は特定できていないが、monitor モード有効化後の内部的なインフラマイグレーションが影響している可能性がある。

知見 3: encryption-status 2(アプリケーション層暗号化)は一般的な HTTPS では記録されない。 ドキュメントを再確認すると、2 は「TCP port 443 for interface endpoint to AWS service」と「TCP port 443 for gateway endpoint」に限定されている。つまり、PrivateLink 経由の AWS サービスアクセスのみが 2 として記録され、EC2 インスタンス間の自己署名証明書による HTTPS は 2 にならない。AWS はパケットの中身を検査せず、ポートとエンドポイントの種類で判定している。

検証 2: Enforce モードへの移行とブロック挙動の確認

Monitor モードで暗号化状況を可視化できた。次のステップとして、暗号化を強制する enforce モードへの移行を試みる。

ブロッキングリソースの特定

Enforce モードに移行する前に、暗号化をブロックしているリソースを特定する必要がある。

Terminal
aws ec2 get-vpc-resources-blocking-encryption-enforcement \
  --vpc-id vpc-09456d794ff62c982
Output
{
    "NonCompliantResources": [
        {
            "Id": "igw-03f9ce2d3fb1b1155",
            "Type": "internet-gateway",
            "IsExcludable": true
        }
    ]
}

ブロッキングリソースは Internet Gateway のみだった。t2.micro(非 Nitro)はブロッキングリソースとして検出されなかった。検証 1 で t2.micro のトラフィックが encryption-status 1 で記録されていた点と合わせると、AWS が内部的に暗号化対応として扱っている可能性があるが、正確な理由は不明である。

Enforce モードへの切り替え

IGW の除外を設定しつつ enforce モードに切り替える。Internet Gateway はトラフィックが AWS ネットワーク外に出るため Nitro 暗号化の対象外であり、enforce モードでは除外設定(Exclusion)が必要になる。除外可能なリソースは IGW、NAT Gateway、Egress-only IGW、VPC Peering、Virtual Private Gateway、Lambda、VPC Lattice、EFS の 8 種類に限定されている。

Terminal
aws ec2 modify-vpc-encryption-control \
  --vpc-encryption-control-id vpcec-070c7cd71ca25ecc8 \
  --mode enforce \
  --internet-gateway-exclusion enable
Output
{
    "VpcEncryptionControl": {
        "Mode": "monitor",
        "State": "enforce-in-progress",
        "StateMessage": "modifying",
        "ResourceExclusions": {
            "InternetGateway": {
                "State": "enabling",
                "StateMessage": "in-progress"
            }
        }
    }
}

注目すべきは、コマンド実行直後の Mode がまだ monitor で、Stateenforce-in-progress である点だ。enforce への切り替えは非同期処理であり、即座には完了しない。

15分後に enforce-failed

以下のコマンドで60秒ごとにステータスをポーリングした。

Terminal (ステータスポーリング)
while true; do
  STATUS=$(aws ec2 describe-vpc-encryption-controls \
    --vpc-ids $VPC_ID \
    --query 'VpcEncryptionControls[0].[Mode,State]' --output text)
  echo "$(date +%H:%M:%S) - $STATUS"
  echo "$STATUS" | grep -q "available\|failed" && break
  sleep 60
done

約15分後に enforce-failed となった。

Output (ポーリング結果)
19:44:33 - monitor  enforce-in-progress
19:45:35 - monitor  enforce-in-progress
...(中略)...
19:58:55 - monitor  enforce-failed
Output (失敗メッセージ)
Failed due to one or more non-compliant resources.
Use GetVpcResourcesBlockingEncryptionEnforcement to discover the blocking resources.

しかし、get-vpc-resources-blocking-encryption-enforcement を再実行しても IGW のみが表示される。IGW の除外は指定済みなのに失敗している。

IGW をデタッチしても失敗

IGW をデタッチしてブロッキングリソースをゼロにした状態で再試行したが、再び約15分後に enforce-failed となった。

Terminal
aws ec2 detach-internet-gateway \
  --internet-gateway-id igw-03f9ce2d3fb1b1155 \
  --vpc-id vpc-09456d794ff62c982
 
aws ec2 get-vpc-resources-blocking-encryption-enforcement \
  --vpc-id vpc-09456d794ff62c982
# → NonCompliantResources: []
 
aws ec2 modify-vpc-encryption-control \
  --vpc-encryption-control-id vpcec-070c7cd71ca25ecc8 \
  --mode enforce
# → 再び約15分後に enforce-failed

ブロッキングリソース API が空を返すにもかかわらず enforce が失敗する。VPC 内に残っているリソースは EC2 インスタンス(m7g + t2)と SSM 用 VPC エンドポイント 3 つの ENI だった。

原因の特定: VPC エンドポイントの ENI

原因を切り分けるため、新しい VPC を作成し、m7g.medium サーバー + t4g.medium クライアント + SSM 用 VPC エンドポイント 3 つの構成で enforce を試みた。同様に約15分で失敗した。次に、SSM 用の VPC エンドポイント(ssm, ssmmessages, ec2messages)を削除した状態で enforce を再試行した。

Terminal
# VPC エンドポイントの削除
aws ec2 delete-vpc-endpoints \
  --vpc-endpoint-ids $SSM_VPCE_ID $SSMMSG_VPCE_ID $EC2MSG_VPCE_ID
 
# ENI が解放されるまで待機(約2分)
# VPC エンドポイント削除後に enforce を再試行
aws ec2 modify-vpc-encryption-control \
  --vpc-encryption-control-id $VPCEC_ID \
  --mode enforce

約15分後に enforce / available に遷移し、enforce モードへの移行が成功した

Output (VPC エンドポイント削除後のポーリング結果)
21:40:32 - monitor  enforce-in-progress
...(中略)...
21:54:55 - enforce  available

つまり、VPC エンドポイントの ENI が enforce 移行をブロックしていた。ドキュメントには「monitor モードを有効化すると、NLB、ALB、Fargate、EKS が自動的に暗号化対応ハードウェアにマイグレーションされる」と記載されているが、VPC エンドポイントの ENI についてはマイグレーションに関する記載がない。本検証では monitor モード有効化から約1時間後でも VPC エンドポイントが存在する状態では enforce が失敗し続けた。マイグレーションにさらに時間がかかる可能性もあるが、get-vpc-resources-blocking-encryption-enforcement でも検出されないため、待つべきか削除すべきかの判断が困難である。

Enforce モードでのブロック挙動

Enforce モードが有効な VPC で、暗号化非対応のインスタンスタイプ(t4g.medium)の起動を試みた。

Terminal
aws ec2 run-instances --image-id $ARM_AMI --instance-type t4g.medium \
  --subnet-id $PRIVATE_SUBNET_ID --security-group-ids $SG_ID \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=enforce-test-t4g}]'
Output
An error occurred (DisallowedByVpcEncryptionControl) when calling the RunInstances operation:
Instance type does not support required VPC encryption

t4g.medium(Nitro v2、暗号化非対応)の起動は DisallowedByVpcEncryptionControl エラーでブロックされた。一方、m7g.medium(Nitro v4、暗号化対応)は同じコマンドで --instance-type m7g.medium に変更するだけで正常に起動できた。

これは実運用で重要な知見である。VPC エンドポイントを使用している環境で enforce モードに移行するには、一時的に VPC エンドポイントを削除するか、VPC エンドポイントの ENI マイグレーションが完了するまで待つ必要がある。ただし、マイグレーション完了のタイミングを確認する API は提供されていない。

Monitor vs Enforce: 移行判断のポイント

比較テーブル

観点MonitorEnforce
有効化の所要時間約1分15分以上(非同期、失敗の可能性あり)
既存トラフィックへの影響なし非暗号化トラフィックがブロックされる
必要な事前準備なしブロッキングリソースの特定・移行・除外設定
Flow Logs の encryption-status✓ 利用可能✓ 利用可能
非 Nitro インスタンスの起動可能ブロックされる(DisallowedByVpcEncryptionControl エラー)
VPC エンドポイントの影響なしENI が enforce 移行をブロックする(削除が必要)

料金の考察

VPC Encryption Controls の料金は非空 VPC あたりの時間課金である。

リージョン時間単価月額(730h)
us-east-1$0.15$109.50
ap-northeast-1 (東京)料金表に未掲載
eu-west-1 (アイルランド)$0.16$116.80

VPC が 10 個ある環境では月額 $1,095 になる。Transit Gateway の暗号化サポートを有効化すると、TGW に接続された全 VPC に課金が発生する点にも注意が必要である。

この料金が妥当かどうかは、コンプライアンス要件の厳しさによる。HIPAA や PCI DSS で暗号化証跡が求められる環境では、手動での暗号化監査コスト(人件費 + ツール費)と比較して判断すべきである。monitor モードだけでも Flow Logs に暗号化ステータスが記録されるため、監査証跡としての価値は十分にある。

まとめ

  • Monitor モードは即日導入できる — 有効化は約1分、既存トラフィックへの影響ゼロ。Flow Logs に encryption-status が追加されるだけなので、本番環境でも安全に有効化できる
  • encryption-status は Nitro バージョンに依存する — Nitro v3 以降(m7g 等)は 1、Nitro v2(t4g 等)は 0 が記録される。ただし t2.micro(非 Nitro)が 1 を返すケースも確認されており、インスタンスタイプごとの挙動は自環境で実測すべきである
  • encryption-status 2 は PrivateLink 経由のみ — 一般的な HTTPS トラフィックは 2 にならない。AWS はパケットの中身ではなく、エンドポイントの種類とポートで判定している
  • Enforce モードへの移行は VPC エンドポイントの ENI にブロックされる — VPC エンドポイントが存在する状態では enforce が失敗し続けた。VPC エンドポイントを削除した状態では約15分で enforce が成功した。get-vpc-resources-blocking-encryption-enforcement では VPC エンドポイントの ENI は検出されないため、原因の特定が困難である。VPC エンドポイントを使用する環境では、一時的な削除・再作成を含む移行計画が必要になる

クリーンアップ

検証リソースの削除手順
Terminal
# Encryption Controls の削除
aws ec2 delete-vpc-encryption-control \
  --vpc-encryption-control-id vpcec-070c7cd71ca25ecc8
 
# EC2 インスタンスの終了
aws ec2 terminate-instances \
  --instance-ids i-0ae5ecc423253d10a i-0a103bc51557301f1 i-05598f623192f9fed
 
# VPC エンドポイントの削除
aws ec2 delete-vpc-endpoints \
  --vpc-endpoint-ids vpce-0af9e55053d66697c vpce-09b65419d5598c9a9 vpce-0e76869f0adbce959
 
# Flow Logs の削除
aws ec2 delete-flow-logs --flow-log-ids fl-020b63347a112dc03
aws logs delete-log-group --log-group-name /vpc/enc-verify-flow-logs
 
# セキュリティグループ・サブネット・VPC の削除
aws ec2 delete-security-group --group-id sg-087c5ea932b375258
aws ec2 delete-subnet --subnet-id subnet-0fbfc4d4b0df3b641
aws ec2 delete-internet-gateway --internet-gateway-id igw-03f9ce2d3fb1b1155
aws ec2 delete-vpc --vpc-id vpc-09456d794ff62c982
 
# IAM リソースの削除
aws iam delete-role-policy --role-name enc-verify-flow-logs-role --policy-name flow-logs-publish
aws iam delete-role --role-name enc-verify-flow-logs-role
aws iam detach-role-policy --role-name enc-verify-ssm-role --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam remove-role-from-instance-profile --instance-profile-name enc-verify-ssm-profile --role-name enc-verify-ssm-role
aws iam delete-instance-profile --instance-profile-name enc-verify-ssm-profile
aws iam delete-role --role-name enc-verify-ssm-role

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援をしており、クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事