零信任是不以「因為在內部網路中,所以就信任」為前提,而是在每次存取時都確認身分與權限的思維。
本文會聚焦在 AWS 上要建立哪些服務、哪些資源,才能把零信任落地。整體架構分成 5 個部分,依序列出各部分要建立的資源與 AWS CLI 指令。
先說明一下,本文中的 AWS CLI 指令,是依據官方文件與 AWS CLI 2.38.4 的說明確認語法,並非實際在帳戶中執行後的結果。
實際在本機動手驗證的只有第 1 部分中,使用 openssl 建立憑證的步驟。
各部分的「確認事項」是根據官方文件推導出的預期行為。
這 5 個部分分別對應零信任中的「誰」「做什麼」「從哪裡來」「記錄」。
部分確認的對象使用的服務1AWS 外部的伺服器AWS IAM Roles Anywhere2服務間的呼叫Amazon VPC Lattice、Amazon Verified Permissions3資料的路徑VPC 端點、Amazon S3 的儲存貯體政策4人的存取AWS Verified Access、AWS IAM Identity Center5記錄與偵測AWS CloudTrail、Amazon GuardDuty、AWS Security Hub CSPM各部分彼此獨立,因此只做需要的部分即可。
所有資源名稱都以 zt- 開頭,方便後續清理時辨識。

後續指令會使用以下變數。
export AWS_REGION=ap-northeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
VPC_ID=vpc-xxxxxxxx # 請替換成你自己的 VPC ID
以下包含按時間計費的資源。
單價是查詢美國東部的價格頁面得知,未確認東京區域的單價。
AWS Verified Access 的單價較高,因此建議第 4 部分確認完成後立即刪除。
如果把存取金鑰放在內部機房或其他雲端的伺服器上,一旦外洩,就會在沒有期限的情況下被使用。
使用 AWS IAM Roles Anywhere,伺服器可以用 X.509 憑證證明身分,並取得有時效性的認證資訊。
要建立的資源如下:

雖然也可以使用 AWS Private Certificate Authority,但因為會產生月費,所以這裡改用 openssl 建立 CA。
# 憑證授權單位
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 對憑證有以下要求:
這個步驟是在 OpenSSL 4.0.3 上執行,並確認產生的憑證符合上述條件。
將 CA 的憑證註冊進去。若直接把 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
若要讓憑證失效,則匯入撤銷清單。
aws rolesanywhere import-crl --name zt-crl --enabled \
--crl-data fileb://crl.pem --trust-anchor-arn $TA_ARN
匯入的撤銷清單不會檢查有效期限,因此每次 CA 更新後都需要重新匯入。
另外也可以設定為自動從憑證中記載的發佈位置取得撤銷清單。
避免出現「因為在同一個 VPC 裡,所以就能呼叫」的狀況。
使用 Amazon VPC Lattice 判定「哪個角色可以呼叫這個服務」,並使用 Amazon Verified Permissions 在應用程式內判定「是否允許這項操作」。
要建立的資源如下:

Cedar 是用來撰寫授權規則的語言。
先把會出現的呼叫者、操作、資源型別定義為 schema。
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
建立一則允許與一則禁止。
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
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 用來呼叫該函數的權限會自動加入。
驗證政策要分別套用在服務網路與服務本身兩個地方。
在服務網路端,先一次排除未簽章的請求。
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)"
有三個注意事項:
呼叫端 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
這三個結果可驗證:在通訊入口與應用程式內部,確實進行了兩層判定。
即使身分正確,也希望避免資料能從非預期路徑被存取。
將 VPC 端點與儲存貯體政策搭配,讓 Amazon S3 只接受「經過這個 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
即使第 1 部分建立的角色對這個儲存貯體有權限,只要不是經由端點,就會被拒絕。
這代表身分確認與路徑確認,是在不同層次上疊加進行的。
VPN 會讓已連線的裝置取得整個網路的可達性。
AWS Verified Access 則是以應用程式為單位,逐次針對每個請求將使用者資訊與政策比對。
要建立的資源如下:

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 的安全群組只允許來自端點安全群組的流量。
如果也想把裝置狀態(例如 OS 更新狀況或風險評分)納入條件,可加入 CrowdStrike、Jamf、JumpCloud 其中之一作為裝置信任提供者。
一個執行個體可以掛一個使用者信任提供者,以及多個裝置信任提供者。
前面 4 個部分都是在存取當下進行判定的機制。
在零信任中,不只要在允許當下確認,之後也要持續確認。
這些材料就是 API 操作記錄,以及對可疑使用方式的偵測。
要建立的資源如下:

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
aws guardduty create-detector --enable
aws securityhub enable-security-hub --enable-default-standards
只要啟用,Amazon GuardDuty 就會開始分析以下三種資料來源:
即使沒有自行設定證跡或流量記錄,Amazon GuardDuty 也會透過獨立途徑取得這些資料。
Amazon GuardDuty 具有一種偵測結果:當發給 EC2 執行個體的暫時認證資訊被拿到 AWS 外部使用時,會標記為 InstanceCredentialExfiltration.OutsideAWS。
即使憑證本身是合法的,只要使用方式不自然,就會被視為異常,這正是零信任的思維直接反映在偵測上的例子。
為了停止計費,先刪除按時間計費的資源。
其餘資源(IAM 角色、信任錨點、設定檔、權限存放區、Lambda 函數、VPC 端點、儲存貯體、證跡)請依名稱前綴 zt- 逐一刪除。
一開始提到,本文聚焦於「要建立哪個服務的哪個資源,才能形成整體」。
整理後可分成以下 4 層,以及第 5 部分的記錄機制。
每一層的設計都相同:預設拒絕,只放行明確允許的項目。
把這種設計一層一層疊起來,就是 AWS 上的零信任架構。
感謝閱讀到最後。