はじめに
ゼロトラストは「社内ネットワークの中にいるから信頼する」という前提を置かず、アクセスのたびに身元と権限を確認する考え方です。
この記事では、AWSでどのサービスのどのリソースを作れば形になるのかに絞って整理します。構成を5つのパートに分け、それぞれで作るリソースとAWS CLIのコマンドを順に並べます。
-
パート1: AWS外のサーバーの身元を証明書で確認する
-
パート2: サービス間の呼び出しを1件ずつ認可する
-
パート3: データへの経路を絞る
-
パート4: 人からアプリケーションへのアクセスをVPNなしで判定する
-
パート5: 操作を記録して検知する
先にお断りしておくと、この記事のAWS CLIコマンドは、公式ドキュメントとAWS CLI 2.38.4のヘルプで構文を確認したもので、実際のアカウントで実行した結果ではありません。
手元で動かして確認したのは、パート1のopensslによる証明書作成だけです。
各パートの「確認すること」は、公式ドキュメントの記載から導いた期待される動作として書いています。
全体の構成
5つのパートは、ゼロトラストの「誰が」「何を」「どこから」「記録」にそれぞれ対応しています。
| パート | 確認する対象 | 使うサービス |
|---|---|---|
| 1 | AWS外のサーバー | AWS IAM Roles Anywhere |
| 2 | サービス間の呼び出し | Amazon VPC Lattice、Amazon Verified Permissions |
| 3 | データへの経路 | VPCエンドポイント、Amazon S3のバケットポリシー |
| 4 | 人のアクセス | AWS Verified Access、AWS IAM Identity Center |
| 5 | 記録と検知 | AWS CloudTrail、Amazon GuardDuty、AWS Security Hub CSPM |
パート同士は独立しているので、必要なパートだけ作れます。
リソース名はすべて zt- で始めて、後片付けのときに見分けやすくしています。
前提
- AWS CLI v2、jq、opensslが使える端末
- 東京リージョン(ap-northeast-1)のVPC、プライベートサブネット、そこで動くAmazon EC2インスタンス
- 管理者相当の権限を持つIAMロール
以降のコマンドでは、次の変数を使います。
export AWS_REGION=ap-northeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
VPC_ID=vpc-xxxxxxxx # 自分のVPCのIDに置き換えます
費用がかかるリソース
時間単位で課金されるリソースが含まれます。
単価は米国東部の料金ページで確認したもので、東京リージョンの単価は確認していません。
- Amazon VPC Lattice: 1サービスあたり0.025ドル/時間
- AWS Verified Access: 1アプリケーションあたり0.27ドル/時間(1時間未満は切り上げ)
- Amazon Verified Permissions: 認可リクエスト1件あたり0.000005ドル
AWS Verified Accessは単価が高いので、パート4は確認が終わったらすぐ削除することをおすすめします。
パート1 AWS外のサーバーの身元を証明書で確認する
オンプレミスや他クラウドのサーバーにアクセスキーを置くと、漏えいしたときに期限なく使われてしまいます。
AWS IAM Roles Anywhereを使うと、サーバーはX.509証明書で身元を示し、期限つきの認証情報を受け取れます。
作るリソースは次のとおりです。
- 自前の認証局(opensslで作成)
- 信頼アンカー zt-anchor(信頼する認証局の登録)
- IAMロール zt-workload-role
- プロファイル zt-profile(引き受けられるロールの定義)
認証局とサーバー証明書の作成
AWS Private Certificate Authorityを使う方法もありますが、月額料金がかかるため、ここではopensslで認証局を作ります。
# 認証局
openssl genrsa -out ca.key 4096
openssl req -new -key ca.key -subj "/CN=zt-root-ca" -out ca.csr
printf "basicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign\n" > ca.ext
openssl x509 -req -in ca.csr -signkey ca.key -sha256 -days 365 -extfile ca.ext -out ca.pem
# サーバー証明書(共通名 server01)
openssl genrsa -out server01.key 2048
openssl req -new -key server01.key -subj "/CN=server01" -out server01.csr
printf "basicConstraints=CA:FALSE\nkeyUsage=critical,digitalSignature\n" > server01.ext
openssl x509 -req -in server01.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-sha256 -days 30 -extfile server01.ext -out server01.pem
拡張の指定には理由があります。
AWS IAM Roles Anywhereは、証明書に次の条件を求めます。
- 認証局の証明書は、基本制約がCA:TRUEで、鍵用途に証明書への署名を含む
- サーバー証明書はX.509v3で、鍵用途にデジタル署名を含む
- 署名アルゴリズムはSHA256以上(MD5とSHA1は拒否される)
この手順はOpenSSL 4.0.3で実行し、上の条件どおりの証明書ができることを確認しました。
信頼アンカーの作成
認証局の証明書を登録します。PEMをそのまま引数に書くと改行の扱いが難しいので、jqでJSONファイルにします。
jq -n --rawfile cert ca.pem \
'{sourceType:"CERTIFICATE_BUNDLE",sourceData:{x509CertificateData:$cert}}' > anchor-source.json
TA_ARN=$(aws rolesanywhere create-trust-anchor --name zt-anchor --enabled \
--source file://anchor-source.json \
--query trustAnchor.trustAnchorArn --output text)
ロールの作成
信頼ポリシーには、証明書の共通名と信頼アンカーの両方を条件に書きます。
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "rolesanywhere.amazonaws.com"},
"Action": ["sts:AssumeRole", "sts:TagSession", "sts:SetSourceIdentity"],
"Condition": {
"StringEquals": {"aws:PrincipalTag/x509Subject/CN": "server01"},
"ArnEquals": {"aws:SourceArn": "$TA_ARN"}
}
}]
}
EOF
aws iam create-role --role-name zt-workload-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy --role-name zt-workload-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
共通名の条件は省略できません。
AWS IAM Roles Anywhereの信頼の境界はアカウント単位で、条件を書かないと、アカウント内のどの信頼アンカーが発行した証明書でも、このロールを引き受けられるためです。
プロファイルの作成
PROFILE_ARN=$(aws rolesanywhere create-profile --name zt-profile --enabled \
--role-arns arn:aws:iam::$ACCOUNT_ID:role/zt-workload-role \
--query profile.profileArn --output text)
サーバー側の設定
サーバーには、AWSが配布している認証情報ヘルパー(aws_signing_helper)を置きます。
ダウンロード先は公式ドキュメントの「Get temporary security credentials from IAM Roles Anywhere」に載っています。
AWS CLIの設定ファイルに、ヘルパーを呼び出すプロファイルを書きます。
[profile zt-server01]
region = ap-northeast-1
credential_process = ./aws_signing_helper credential-process --certificate server01.pem --private-key server01.key --trust-anchor-arn <TA_ARNの値> --profile-arn <PROFILE_ARNの値> --role-arn arn:aws:iam::<アカウントID>:role/zt-workload-role
確認すること
aws sts get-caller-identity --profile zt-server01
- zt-workload-roleを引き受けたセッションのARNが返る
- 共通名を server02 にした証明書では、ロールの引き受けが拒否される
- 認証情報の有効期間は15分から12時間の範囲で指定できる
証明書を失効させたい場合は、失効リストを取り込みます。
aws rolesanywhere import-crl --name zt-crl --enabled \
--crl-data fileb://crl.pem --trust-anchor-arn $TA_ARN
取り込んだ失効リストは有効期限を検査されないので、認証局が更新するたびに取り込み直す必要があります。
証明書に書かれた配布先から失効リストを自動で取得する設定もあります。
パート2 サービス間の呼び出しを1件ずつ認可する
同じVPCにいるから呼び出せる、という状態をなくします。
Amazon VPC Latticeで「どのロールがこのサービスを呼べるか」を判定し、Amazon Verified Permissionsで「その操作をしてよいか」をアプリケーション内で判定します。
作るリソースは次のとおりです。
- ポリシーストア(Cedarのスキーマとポリシーの置き場)
- AWS Lambda関数 zt-orders-fn(呼び出される側)
- サービスネットワーク zt-network とサービス zt-orders
- 呼び出し側のIAMロール zt-client-role
ポリシーストアとスキーマの作成
Cedarは認可ルールを書くための言語です。
まず、登場する呼び出し元、操作、リソースの型をスキーマとして定義します。
STORE_ID=$(aws verifiedpermissions create-policy-store \
--validation-settings mode=STRICT \
--query policyStoreId --output text)
cat > schema.json <<'EOF'
{
"ZeroTrust": {
"entityTypes": {"Caller": {}, "Order": {}},
"actions": {
"ReadOrder": {"appliesTo": {"principalTypes": ["Caller"], "resourceTypes": ["Order"]}},
"UpdateOrder": {"appliesTo": {"principalTypes": ["Caller"], "resourceTypes": ["Order"]}}
}
}
}
EOF
jq -n --rawfile s schema.json '{cedarJson:$s}' > schema-def.json
aws verifiedpermissions put-schema --policy-store-id $STORE_ID \
--definition file://schema-def.json
Cedarポリシーの作成
許可を1つ、禁止を1つ作ります。
cat > permit.cedar <<'EOF'
permit(
principal == ZeroTrust::Caller::"zt-client-role",
action in [ZeroTrust::Action::"ReadOrder", ZeroTrust::Action::"UpdateOrder"],
resource
);
EOF
cat > forbid.cedar <<'EOF'
forbid(
principal,
action == ZeroTrust::Action::"UpdateOrder",
resource == ZeroTrust::Order::"1001"
);
EOF
for f in permit forbid; do
jq -n --rawfile p $f.cedar '{static:{statement:$p}}' > $f-def.json
aws verifiedpermissions create-policy --policy-store-id $STORE_ID \
--definition file://$f-def.json
done
この時点で、CLIから判定を確かめられます。
aws verifiedpermissions is-authorized --policy-store-id $STORE_ID \
--principal entityType=ZeroTrust::Caller,entityId=zt-client-role \
--action actionType=ZeroTrust::Action,actionId=UpdateOrder \
--resource entityType=ZeroTrust::Order,entityId=1001
- 注文1001の更新はDENYになる(禁止が許可より優先される)
- 注文1002の更新はALLOWになる
- ポリシーに書いていない呼び出し元はDENYになる(既定は拒否)
呼び出される側のLambda関数の作成
Amazon VPC Latticeは、認証できた呼び出し元のARNをイベントに入れてLambda関数へ渡します。
関数はそのロール名を取り出し、ポリシーストアに判定を問い合わせます。
# app.py
import json, os, boto3
avp = boto3.client("verifiedpermissions")
STORE_ID = os.environ["POLICY_STORE_ID"]
def handler(event, context):
principal = event["requestContext"]["identity"].get("principal", "")
# 例: arn:aws:iam::123456789012:assumed-role/zt-client-role/i-0123...
caller = principal.split("/")[1] if "assumed-role/" in principal else "anonymous"
action = "ReadOrder" if event["method"] == "GET" else "UpdateOrder"
order_id = event["path"].split("?")[0].rstrip("/").split("/")[-1]
decision = avp.is_authorized(
policyStoreId=STORE_ID,
principal={"entityType": "ZeroTrust::Caller", "entityId": caller},
action={"actionType": "ZeroTrust::Action", "actionId": action},
resource={"entityType": "ZeroTrust::Order", "entityId": order_id},
)["decision"]
return {
"isBase64Encoded": False,
"statusCode": 200 if decision == "ALLOW" else 403,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"caller": caller, "action": action, "decision": decision}),
}
実行ロールには、ログ出力と判定APIの権限を付けます。
cat > lambda-trust.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
"Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}
EOF
aws iam create-role --role-name zt-orders-fn-role \
--assume-role-policy-document file://lambda-trust.json
aws iam attach-role-policy --role-name zt-orders-fn-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
cat > avp-policy.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
"Action":"verifiedpermissions:IsAuthorized",
"Resource":"arn:aws:verifiedpermissions::$ACCOUNT_ID:policy-store/$STORE_ID"}]}
EOF
aws iam put-role-policy --role-name zt-orders-fn-role \
--policy-name zt-avp --policy-document file://avp-policy.json
zip fn.zip app.py
FN_ARN=$(aws lambda create-function --function-name zt-orders-fn \
--runtime python3.13 --handler app.handler \
--role arn:aws:iam::$ACCOUNT_ID:role/zt-orders-fn-role \
--zip-file fileb://fn.zip \
--environment "Variables={POLICY_STORE_ID=$STORE_ID}" \
--query FunctionArn --output text)
サービスとサービスネットワークの作成
ターゲットグループにLambda関数を登録し、サービスのリスナーから転送します。
イベントの形式は、呼び出し元の情報が入るV2を指定します。
TG_ID=$(aws vpc-lattice create-target-group --name zt-orders-tg --type LAMBDA \
--config lambdaEventStructureVersion=V2 --query id --output text)
aws vpc-lattice register-targets --target-group-identifier $TG_ID \
--targets id=$FN_ARN
SVC_ID=$(aws vpc-lattice create-service --name zt-orders --auth-type AWS_IAM \
--query id --output text)
cat > listener-config.json <<EOF
{"forward":{"targetGroups":[{"targetGroupIdentifier":"$TG_ID","weight":100}]}}
EOF
aws vpc-lattice create-listener --service-identifier $SVC_ID --name https-443 \
--protocol HTTPS --port 443 --default-action file://listener-config.json
SN_ID=$(aws vpc-lattice create-service-network --name zt-network --auth-type AWS_IAM \
--query id --output text)
aws vpc-lattice create-service-network-service-association \
--service-identifier $SVC_ID --service-network-identifier $SN_ID
aws vpc-lattice create-service-network-vpc-association \
--service-network-identifier $SN_ID --vpc-identifier $VPC_ID
公式ドキュメントによると、CLIでLambda関数を登録すると、Amazon VPC Latticeが関数を呼び出すための権限は自動で追加されます。
認証ポリシーの設定
認証ポリシーは、サービスネットワークとサービスの2か所に付けます。
サービスネットワーク側では、署名のないリクエストをまとめて除外します。
cat > sn-auth.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "*",
"Condition": {"StringNotEquals": {"aws:PrincipalType": "Anonymous"}}
}]
}
EOF
aws vpc-lattice put-auth-policy --resource-identifier $SN_ID \
--policy "$(jq -c . sn-auth.json)"
サービス側では、呼び出せるロールを zt-client-role だけに絞ります。
SVC_ARN=$(aws vpc-lattice get-service --service-identifier $SVC_ID --query arn --output text)
cat > svc-auth.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::$ACCOUNT_ID:role/zt-client-role"},
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "$SVC_ARN/*"
}]
}
EOF
aws vpc-lattice put-auth-policy --resource-identifier $SVC_ID \
--policy "$(jq -c . svc-auth.json)"
注意点が3つあります。
- 認証タイプをAWS_IAMにしただけでは不十分で、匿名を除外する条件を書かないと、ポリシー次第で署名なしのリクエストも通る
- 認証ポリシーのJSONには改行を含められないため、jq -c で1行にしてから渡す
- 呼び出し側のロールにも、vpc-lattice-svcs:Invoke を許可するIAMポリシーが必要になる
呼び出し側のEC2インスタンスに付けるロール zt-client-role には、次のポリシーを付けます。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "*"
}]
}
確認すること
サービスのドメイン名を取得します。
aws vpc-lattice get-service --service-identifier $SVC_ID \
--query dnsEntry.domainName --output text
VPC内のEC2インスタンスから、SigV4(AWS APIと同じ署名方式)で署名して呼び出します。Amazon VPC Latticeは本文への署名に対応していないので、ヘッダーにUNSIGNED-PAYLOADを指定します。
# call.py (pip install botocore awscrt requests)
import sys, requests, botocore.session
from botocore import crt
from botocore.awsrequest import AWSRequest
url, method = sys.argv[1], sys.argv[2]
session = botocore.session.Session()
signer = crt.auth.CrtSigV4Auth(session.get_credentials(), "vpc-lattice-svcs", "ap-northeast-1")
headers = {"x-amz-content-sha256": "UNSIGNED-PAYLOAD"}
request = AWSRequest(method=method, url=url, headers=headers)
request.context["payload_signing_enabled"] = False
signer.add_auth(request)
prepped = request.prepare()
response = requests.request(method, prepped.url, headers=prepped.headers)
print(response.status_code, response.text)
python3 call.py https://<サービスのドメイン名>/orders/1001 GET
python3 call.py https://<サービスのドメイン名>/orders/1001 PUT
curl https://<サービスのドメイン名>/orders/1001
- 署名つきのGETは、判定がALLOWで返る
- 署名つきのPUTは、Amazon VPC Latticeを通過したうえで、Cedarの禁止によりDENYで返る
- 署名なしのcurlは、Lambda関数に届く前にアクセス拒否になる
通信の入口と、アプリケーションの中の2段階で判定していることが、この3つの結果で確かめられます。
パート3 データへの経路を絞る
身元が正しくても、想定外の経路からデータに届く状態は避けたいところです。
VPCエンドポイントとバケットポリシーを組み合わせて、「このVPCのこのエンドポイントを通ったリクエストだけ」をAmazon S3が受け付けるようにします。
作るリソースは次のとおりです。
- Amazon S3バケット zt-data-<アカウントID>
- ゲートウェイ型のVPCエンドポイント(エンドポイントポリシーつき)
- バケットポリシー
VPCエンドポイントの作成
エンドポイントポリシーで、このエンドポイントから届く先を対象のバケットだけに絞ります。
BUCKET=zt-data-$ACCOUNT_ID
aws s3api create-bucket --bucket $BUCKET \
--create-bucket-configuration LocationConstraint=ap-northeast-1
cat > endpoint-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"]
}]
}
EOF
VPCE_ID=$(aws ec2 create-vpc-endpoint --vpc-endpoint-type Gateway \
--vpc-id $VPC_ID --service-name com.amazonaws.ap-northeast-1.s3 \
--route-table-ids rtb-xxxxxxxx \
--policy-document file://endpoint-policy.json \
--query VpcEndpoint.VpcEndpointId --output text)
エンドポイントポリシーを付けない場合、既定のポリシーはすべての操作を許可します。
バケットポリシーの設定
エンドポイント以外から来たリクエストを拒否します。条件を誤ると自分も操作できなくなるので、管理用のロールを例外にしておきます。
cat > bucket-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyUnlessFromVpce",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"],
"Condition": {
"StringNotEquals": {"aws:SourceVpce": "$VPCE_ID"},
"ArnNotLike": {"aws:PrincipalArn": "arn:aws:iam::$ACCOUNT_ID:role/<管理用のロール名>"}
}
}]
}
EOF
aws s3api put-bucket-policy --bucket $BUCKET --policy file://bucket-policy.json
確認すること
- VPC内のEC2インスタンスからは、バケットの一覧取得と読み書きができる
- 同じ認証情報をVPCの外(手元の端末など)で使うと、アクセス拒否になる
- VPC内のEC2インスタンスから別のバケットへは、エンドポイントポリシーにより届かない
パート1で作ったロールにこのバケットへの権限があっても、エンドポイントを通らない限り拒否されます。
身元の確認と経路の確認を、別々の層で重ねていることになります。
パート4 人からアプリケーションへのアクセスをVPNなしで判定する
VPNは、接続できた端末にネットワーク全体への到達を与えます。
AWS Verified Accessは、アプリケーション単位で、リクエストのたびに利用者の情報をポリシーと照合します。
作るリソースは次のとおりです。
- Verified Accessインスタンス(判定を行う本体)
- 信頼プロバイダー(利用者の情報の提供元)
- グループ(同じ要件のアプリケーションをまとめる単位、ポリシーを持つ)
- エンドポイント(アプリケーションへの入口)
このパートの前提
- 内部向けのApplication Load Balancerと、その後ろで動くアプリケーション
- 公開するドメイン名と、AWS Certificate Managerの証明書
- AWS IAM Identity Centerの組織インスタンス(同じリージョンで有効化されていること)
AWS IAM Identity Centerのアカウントインスタンスは、信頼プロバイダーとして使えません。組織インスタンスがない場合は、OIDCに対応したIdPを信頼プロバイダーにします。
インスタンスと信頼プロバイダーの作成
VA_ID=$(aws ec2 create-verified-access-instance --description zt-va \
--query VerifiedAccessInstance.VerifiedAccessInstanceId --output text)
TP_ID=$(aws ec2 create-verified-access-trust-provider \
--trust-provider-type user \
--user-trust-provider-type iam-identity-center \
--policy-reference-name idc \
--query VerifiedAccessTrustProvider.VerifiedAccessTrustProviderId --output text)
aws ec2 attach-verified-access-trust-provider \
--verified-access-instance-id $VA_ID \
--verified-access-trust-provider-id $TP_ID
policy-reference-name に指定した idc は、次のポリシーの中で利用者の情報を参照する名前になります。
グループとポリシーの作成
AWS IAM Identity Centerの特定のグループに所属し、メールアドレスが検証済みの利用者だけを許可します。グループは名前ではなくIDで指定します。
cat > va-policy.cedar <<'EOF'
permit(principal,action,resource)
when {
context.idc.groups has "<AWS IAM Identity CenterのグループID>"
&& context.idc.user.email.verified == true
};
EOF
VAG_ID=$(aws ec2 create-verified-access-group \
--verified-access-instance-id $VA_ID \
--policy-document file://va-policy.cedar \
--query VerifiedAccessGroup.VerifiedAccessGroupId --output text)
ポリシーを定義するまで、すべてのリクエストは既定で拒否されます。
エンドポイントの作成
aws ec2 create-verified-access-endpoint \
--verified-access-group-id $VAG_ID \
--endpoint-type load-balancer --attachment-type vpc \
--domain-certificate-arn <証明書のARN> \
--application-domain app.example.com \
--endpoint-domain-prefix zt-app \
--security-group-ids sg-xxxxxxxx \
--load-balancer-options Protocol=https,Port=443,LoadBalancerArn=<ALBのARN>,SubnetIds=subnet-aaaa,subnet-bbbb
作成後、公開するドメイン名(app.example.com)のCNAMEレコードを、エンドポイントのドメイン名に向けます。
ALBのセキュリティグループは、エンドポイントのセキュリティグループからの通信だけを許可します。
確認すること
- ブラウザで公開ドメインを開くと、AWS IAM Identity Centerのサインイン画面に移る
- 指定したグループの利用者は、アプリケーションの画面が表示される
- グループに所属していない利用者は、サインインできてもアプリケーションに到達できない
デバイスの状態(OSの更新状況やリスク評価)も条件に加えたい場合は、CrowdStrike、Jamf、JumpCloudのいずれかをデバイスの信頼プロバイダーとして追加します。
1つのインスタンスには、利用者の信頼プロバイダーを1つ、デバイスの信頼プロバイダーを複数付けられます。
パート5 操作を記録して検知する
ここまでの4つのパートは、アクセスの時点で判定する仕組みでした。
ゼロトラストでは、許可した後も継続して確認します。
その材料がAPI操作の記録と、不審な使われ方の検知です。
作るリソースは次のとおりです。
- 証跡 zt-trail(API操作の記録をAmazon S3に保存)
- Amazon GuardDutyの検出器
- AWS Security Hub CSPM(検出結果の集約と設定チェック)
証跡の作成
AWS CloudTrailのイベント履歴で見られるのは、直近90日分の管理イベントだけです。
なので、長く残すために証跡を作ります。
また、保存先のバケットには、AWS CloudTrailからの書き込みを許可するバケットポリシーが必要です。
aws cloudtrail create-trail --name zt-trail \
--s3-bucket-name <ログ保存用のバケット名> \
--is-multi-region-trail --enable-log-file-validation
aws cloudtrail start-logging --name zt-trail
Amazon S3のオブジェクト操作のようなデータイベントは、既定では記録されません。
パート3のバケットへの読み書きを記録する場合は、明示的に追加します。
cat > selectors.json <<EOF
[{
"Name": "zt-data-bucket",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
{"Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::$BUCKET/"]}
]
}]
EOF
aws cloudtrail put-event-selectors --trail-name zt-trail \
--advanced-event-selectors file://selectors.json
Amazon GuardDutyとAWS Security Hub CSPMの有効化
aws guardduty create-detector --enable
aws securityhub enable-security-hub --enable-default-standards
Amazon GuardDutyは、有効化するだけで3つのデータソースの分析を始めます。
- AWS CloudTrailの管理イベント
- VPCフローログ
- Amazon Route 53 ResolverのDNSクエリログ
自分で証跡やフローログを設定していなくても、Amazon GuardDutyは独立した経路でこれらを取得します。
確認すること
- パート1から4で行った操作が、AWS CloudTrailのイベント履歴に呼び出し元つきで残っている
- Amazon GuardDutyの検出結果が、AWS Security Hub CSPMに集約されている
- AWS Security Hub CSPMのセキュリティ基準で、設定の不備が一覧になっている
Amazon GuardDutyには、EC2インスタンスに発行された一時的な認証情報がAWSの外から使われたときの検出結果(InstanceCredentialExfiltration.OutsideAWS)があります。
認証情報が正規のものであっても、使われ方が不自然なら異常として扱う、というゼロトラストの考え方がそのまま検出の形になっています。
後片付け
課金を止めるため、時間課金のあるリソースから削除します。
- パート4: エンドポイント、グループ、信頼プロバイダーの切り離しと削除、インスタンスの順に削除する
- パート2: サービスネットワークの関連付け、リスナー、サービス、ターゲットグループ、サービスネットワークの順に削除する
- パート5: Amazon GuardDutyとAWS Security Hub CSPMは、30日間の無料トライアル後に課金が始まる
残りのリソース(IAMロール、信頼アンカー、プロファイル、ポリシーストア、Lambda関数、VPCエンドポイント、バケット、証跡)は、zt- で始まる名前を目印に削除します。
5つのパートで何が作れたか
冒頭で、どのサービスのどのリソースを作れば形になるのかに絞ると書きました。
整理すると、作るものは次の4層と、パート5の記録の仕組みでした。
- 身元: 証明書とIAMロール(パート1)、AWS IAM Identity Centerの利用者とグループ(パート4)
- 通信の入口: Amazon VPC Latticeの認証ポリシー(パート2)、AWS Verified Accessのポリシー(パート4)
- アプリケーションの中: Amazon Verified PermissionsのCedarポリシー(パート2)
- データへの経路: エンドポイントポリシーとバケットポリシー(パート3)
どの層も「既定は拒否で、明示的に許可したものだけ通す」という同じ作りになっています。
この作りを層ごとに重ねたものが、AWSでのゼロトラスト構成です。
最後まで読んでいただきありがとうございました。





