AWS、Azure 与 Google Cloud CLI:身份、作用域与自动化护栏
删除脚本没有错,错的是它连到了谁
一个开发团队准备清理演示环境。操作者先在 AWS 控制台看过测试账号,又在另一个终端登录了 Azure,最后运行仓库里的清理脚本。脚本语法正确,资源名也带 demo,但 AWS CLI 从残留环境变量读取了另一组凭据,Azure CLI 仍把共享订阅设为默认,gcloud 的活动 configuration 则指向同名生产项目。三个命令都可能成功,成功恰恰成为最危险的结果。
云 CLI 不是“把控制台按钮改成命令”的薄壳。它至少同时处理四层状态:本机安装的客户端版本;人或工作负载取得的凭据;账号、tenant、subscription、project 等资源容器;region、zone、resource group 等资源位置。命令行参数、环境变量、配置文件、登录缓存和运行环境身份又会按各自优先级参与解析。只确认资源名,没有确认调用者和资源容器,就等于在不知道数据库连接串的情况下执行 DROP。
三家对象不能机械对齐。AWS profile 是一组凭据来源和默认配置,最终调用者由 STS 身份决定;Azure 登录发生在 Microsoft Entra tenant 中,资源操作落到 subscription,resource group 是订阅内的生命周期容器;Google Cloud 的 gcloud configuration 把活动账号、project 与属性绑在一起,而 project 既是资源组织边界,也是 API、配额和计费常用边界。region 和 zone 也不是全局通行证:有的服务是全球对象,有的是区域对象,有的是可用区对象。可靠脚本必须逐层显式化,而不是发明一个模糊的 CLOUD_ENV=dev 就假设三家含义相同。
先固定客户端,再谈登录
个人开发机适合本机安装,临时排障可使用各家 CloudShell,CI 则适合在受控 runner 镜像里固定版本。CloudShell 已带 CLI 并继承浏览器会话,启动快,但它的文件、插件和默认作用域可能与仓库基线不同;本机安装便于 IDE 和脚本调用,却要管理升级、代理、证书与登录缓存;容器镜像隔离更强,但缓存挂载和宿主身份注入若处理不当,仍会把凭据带进去。
Windows 上可从 AWS CLI v2 安装页下载官方 MSI,从 Azure CLI Windows 安装页使用 WinGet 或 MSI,从 Google Cloud CLI 安装页选择 Windows 安装器。Linux、macOS、CPU 架构和包管理器入口会变化,应在目标 runner 镜像上按这三页确认,而不是把开发机安装包复制到 CI。
# Azure CLI:Windows 官方文档提供的 WinGet 包
winget install --exact --id Microsoft.AzureCLI
# AWS CLI 与 Google Cloud CLI:下载官方安装器并校验来源后执行
msiexec.exe /i .\AWSCLIV2.msi
.\GoogleCloudSDKInstaller.exe
# 重新打开终端后记录真实基线
aws --version
az version --output json
gcloud version --format=json
# 确认解析到的可执行文件,防止 PATH 中同时存在旧版
Get-Command aws, az, gcloud | Select-Object Name, Source, Version成功标准不是“安装器显示完成”,而是新终端能解析到预期路径,三个版本命令均以退出码 0 返回,并且版本输出被 CI 镜像清单或项目运行记录保存。若 aws --version 显示 v1,应先阅读 AWS 的 v1 到 v2 迁移说明;两个主版本共用 aws 命令名,PATH 顺序会决定实际执行者。若 az 安装后找不到,先重开终端再检查 PATH。若 gcloud 的组件命令缺失,先确认安装形态是否支持 component manager;系统包管理器安装与归档安装的组件管理方式可能不同。
升级也要成套验证:客户端版本、扩展或组件、脚本依赖的输出字段、runner 镜像和认证动作一起进入变更。不要在流水线每次运行时无条件升级到最新版本,那会让同一提交在不同时间得到不同解析器与命令行为。保留上一版安装入口和一组身份、只读查询、格式化、分页回归命令,升级失败时才能真正降级。
用隔离目录证明默认状态并不可信
先做一个不需要云账号的反向实验:为三套 CLI 指向全新的临时配置位置,然后执行身份查询。AWS 支持用 AWS_CONFIG_FILE 和 AWS_SHARED_CREDENTIALS_FILE 改变两个文件位置;Azure 把配置、登录缓存和日志放在 AZURE_CONFIG_DIR 下;gcloud 用 CLOUDSDK_CONFIG 改变配置目录。AWS 的配置优先级不是一句“profile 优先”:命令行选项优先于环境设置,环境中的凭据与 region 又会覆盖配置文件中的同名值,随后才轮到 AssumeRole、IAM Identity Center、共享凭据文件、配置文件和运行环境角色等来源。AWS_PROFILE 只选择命名 profile,不能被当成“已经清除了其他 AWS 凭据变量”的证明。Azure 的配置说明把参数、环境变量、配置文件按高到低排列,但登录缓存中的账号与当前 subscription 还要另行核对。gcloud 的命名 configuration 文档则说明:configuration 决定属性,--configuration 或 CLOUDSDK_ACTIVE_CONFIG_NAME 选择本次调用使用哪一套属性;环境变量形式的 property 高于 configuration 中的值,隔离凭据和配置文件仍要靠独立的 CLOUDSDK_CONFIG 目录。
$sandbox = Join-Path $env:TEMP 'cloud-cli-isolation-<RUN_ID>'
New-Item -ItemType Directory -Force $sandbox | Out-Null
$env:AWS_CONFIG_FILE = Join-Path $sandbox 'aws-config'
$env:AWS_SHARED_CREDENTIALS_FILE = Join-Path $sandbox 'aws-credentials'
$env:AZURE_CONFIG_DIR = Join-Path $sandbox 'azure'
$env:CLOUDSDK_CONFIG = Join-Path $sandbox 'gcloud'
# 新目录中不应继承个人登录状态
aws sts get-caller-identity --region <AWS_REGION> --no-cli-pager
az account show --output json
gcloud auth list --filter=status:ACTIVE --format=json
gcloud config list --format=json预期结果是“明确失败或返回空状态”,而不是任何真实身份。AWS 常见输出是 Unable to locate credentials;Azure 会提示先运行 az login;gcloud 的活动账号列表应为空,配置中也不应凭空出现 <GCP_PROJECT_ID>。这个失败证明配置隔离生效,并不证明网络或云端权限正常。
若隔离目录仍返回了身份,说明还有环境变量、容器元数据、工作负载身份或命令包装器参与解析。先检查当前进程中以 AWS_、AZURE_、CLOUDSDK_、GOOGLE_ 开头的变量,再检查 runner 是否挂载了宿主 home、是否运行在带实例身份的云主机上。不要用 --debug 直接把全量日志上传工单;调试输出可能包含请求头、账号标识、端点、文件路径与令牌元数据。应在隔离终端复现,脱敏后只保留错误码、请求 ID、目标端点和凭据来源判断。
正向实验是在同一个隔离目录中完成一次受控登录,再重复身份查询。这样得到的缓存可以随实验目录一起撤销,不会污染个人默认目录。团队模板应让每个仓库、每个 CI job 使用独立目录;共享 ~/.aws、~/.azure 或 %APPDATA%\gcloud 会把并行作业变成彼此可见的隐式状态机。
AWS:profile 先选凭据来源,STS 再证明调用者
开发人员优先使用组织提供的 IAM Identity Center,而不是把个人 access key 和 secret key 写进 credentials。AWS 的 IAM Identity Center CLI 配置指南给出的主路径是 aws configure sso、aws sso login 和带 profile 的业务命令。SSO 所在 region 是 Identity Center 目录的位置,业务命令的默认 region 是目标服务位置,两者可以不同,不能看到两个 region 字段就随意改成同一个。
[profile <AWS_PROFILE_DEV>]
sso_session = <AWS_SSO_SESSION>
sso_account_id = <AWS_ACCOUNT_ID>
sso_role_name = <AWS_PERMISSION_SET_NAME>
region = <AWS_REGION>
output = json
[sso-session <AWS_SSO_SESSION>]
sso_start_url = https://<AWS_SSO_PORTAL>.awsapps.com/start
sso_region = <AWS_SSO_REGION>
sso_registration_scopes = sso:account:access实际配置可运行 aws configure sso --profile <AWS_PROFILE_DEV> 让向导写入,避免手工拼错 section。登录后先检查配置来源,再调用 STS:
aws sso login --profile <AWS_PROFILE_DEV>
aws configure list --profile <AWS_PROFILE_DEV>
aws sts get-caller-identity \
--profile <AWS_PROFILE_DEV> \
--region <AWS_REGION> \
--output json \
--no-cli-pager可接受的结果应包含占位形态的 Account: "<AWS_ACCOUNT_ID>" 与 Arn: "arn:aws:sts::<AWS_ACCOUNT_ID>:assumed-role/<AWS_ROLE_NAME>/<SESSION_NAME>"。脚本必须比较完整账号 ID 和允许的 role ARN 模式;只检查 profile 名没有意义,因为 profile 是本机可修改标签。get-caller-identity 成功后,再做带显式 region、服务端标签过滤与 JSON 输出的只读查询:
aws ec2 describe-instances \
--profile <AWS_PROFILE_DEV> \
--region <AWS_REGION> \
--filters "Name=tag:owner,Values=<OWNER>" "Name=tag:environment,Values=dev" \
--query 'Reservations[].Instances[].{id:InstanceId,state:State.Name,az:Placement.AvailabilityZone}' \
--output json \
--no-cli-pager预期是 JSON 数组;没有匹配资源时应是空数组,而不是把过滤条件删掉重试。AccessDenied 表示身份已被识别但策略不允许该 API;ExpiredToken 指向 SSO 或角色会话过期;InvalidClientTokenId 常见于无效凭据或不同分区、端点组合;签名时间错误则先检查系统时钟。aws configure list 能显示 region、凭据值的遮罩和来源,是排查“为什么没用这个 profile”的第一证据。
命令行 --profile 和 --region 应保留在高风险脚本中,但它们不是完整身份断言。环境变量适合一个受控进程的临时默认值,却可能覆盖 profile 内 region;AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKEN 必须按同一会话成组出现,残留其中一部分会造成难以解释的签名失败。脚本发现这些变量时不要打印值,只报告变量名是否存在并终止;随后用 aws configure list --profile ... 观察来源,再以 STS 返回的账号和 ARN 作最终判断。结束人类会话运行 aws sso logout 会清除本机缓存的 IAM Identity Center 会话;共享机器上它可能影响多个 SSO profile,执行前要说明影响。实验使用独立配置目录时,退出后删除该目录更容易证明无残留。
Azure:tenant 负责身份归属,subscription 决定资源落点
Azure CLI 没有与 AWS profile 完全等价的命名凭据对象。交互登录会在配置目录中缓存账号,多个 tenant 和 subscription 可能同时可见;大多数资源命令若不带 --subscription,使用当前活动订阅。微软的登录指南建议人类使用交互登录,云上任务使用 managed identity,外部自动化使用 service principal 或联邦 workload identity。用户密码登录无法承接 MFA,不应成为脚本兜底;service principal 若确实需要本地登录,优先证书而不是 client secret,且不能把 secret 直接写在命令参数、脚本或流水线回显中。
az login --tenant <AZURE_TENANT_ID>
az account list \
--query '[].{tenantId:tenantId,subscriptionId:id,name:name,isDefault:isDefault,state:state}' \
--output json
az account set --subscription <AZURE_SUBSCRIPTION_ID>
az account show \
--subscription <AZURE_SUBSCRIPTION_ID> \
--query '{tenantId:tenantId,subscriptionId:id,name:name,user:user.name,state:state}' \
--output json身份护栏要同时比较 <AZURE_TENANT_ID> 与 <AZURE_SUBSCRIPTION_ID>。订阅显示名可重名、可改名,不能作为唯一判断。az account set 改变配置目录里的活动订阅,其他终端若共用目录就会被影响;脚本在关键命令上继续传 --subscription,不要认为 set 过一次便永久安全。资源位置再通过命令参数或资源本身确认:
az resource list \
--subscription <AZURE_SUBSCRIPTION_ID> \
--resource-group <AZURE_RESOURCE_GROUP> \
--tag owner=<OWNER> \
--query '[].{id:id,name:name,type:type,location:location}' \
--output json预期是目标 resource group 内的 JSON 数组。空数组是有效业务结果;Please run 'az login' 表示缓存中没有可用会话;AADSTS... 错误应保留末尾的 trace/correlation 标识并检查 tenant、条件访问、MFA 和本机时间;SubscriptionNotFound 常见于选错 tenant 或身份未获订阅访问;AuthorizationFailed 表示 token 有效但 RBAC 在该 resource ID 上不允许操作。Azure RBAC 可能刚变更而尚未传播,重试前先核对 role assignment 的主体、scope 与条件,不要临时加 Owner。
Azure 的 location 常作为创建参数,但资源 provider 在不同区域的可用 SKU 和能力并不完全一致。先用目标服务的 location/SKU 查询确认,再创建资源;不能因为 az account list-locations 返回某位置,就推断所有产品都可部署。结束时运行 az logout 退出账号,再运行 az account clear 清空本地 subscription 缓存;如果使用隔离 AZURE_CONFIG_DIR,最后移除该目录。卸载 CLI 不会自动撤销 Entra 应用、federated credential、role assignment 或已有资源,这些必须由各自 owner 单独回收。
gcloud:configuration 管活动账号,project 管资源与计费归属
gcloud init 会引导登录、创建或选择 configuration,并设置 project、Compute region 与 zone 等属性,初始化文档也建议用 gcloud config list 查看结果。多人、多项目环境不要长期依赖 default;为仓库创建命名 configuration,并在命令上用 --configuration 与 --project 固定调用上下文。属性解析遵循“本次命令 flag 高于 property,CLOUDSDK_SECTION_PROPERTY 环境变量高于 configuration property”;因此 --configuration 选对了配置,仍不能省略对 CLOUDSDK_CORE_PROJECT 等冲突变量的检查。
gcloud config configurations create <GCP_CONFIG_DEV>
gcloud config configurations activate <GCP_CONFIG_DEV>
# `auth login` 会把登录者设为当前 configuration 的活动账号。
gcloud auth login <GCP_USER_EMAIL> --configuration=<GCP_CONFIG_DEV>
gcloud config set project <GCP_PROJECT_ID> --configuration=<GCP_CONFIG_DEV>
gcloud config set compute/region <GCP_REGION> --configuration=<GCP_CONFIG_DEV>
gcloud config set compute/zone <GCP_ZONE> --configuration=<GCP_CONFIG_DEV>
gcloud auth list --configuration=<GCP_CONFIG_DEV> --filter=status:ACTIVE --format=json
gcloud config configurations describe <GCP_CONFIG_DEV> --format=json
gcloud projects describe <GCP_PROJECT_ID> \
--configuration=<GCP_CONFIG_DEV> \
--format='json(projectId,projectNumber,name,lifecycleState)'成功时,活动账号应是 <GCP_USER_EMAIL>,configuration 的 core.project 应为 <GCP_PROJECT_ID>,项目状态符合团队允许值。创建 configuration 本身不会切换当前上下文,且命名 configuration 共享同一 CLOUDSDK_CONFIG 目录中的凭据存储;因此并行 job 仍应使用独立目录,关键命令仍显式传 --configuration 与 --project。项目显示名不可靠,应比较 project ID,审计与 IAM 绑定还经常使用不可变的 project number。区域资源查询继续显式传 project 和 zone:
gcloud compute instances list \
--project=<GCP_PROJECT_ID> \
--zones=<GCP_ZONE> \
--filter='labels.owner=<OWNER> AND labels.environment=dev' \
--format='json(name,zone,status,labels)'You do not currently have an active account selected 指向 gcloud CLI 登录状态;The Application Default Credentials are not available 则来自应用或 SDK 的 ADC,二者不是一套登录面。gcloud auth application-default login 会把用户 ADC 放到 well-known location,供客户端库自动发现,ADC 命令参考明确说明了这一点。不要为了让本地应用通过就把服务账号 JSON key 复制进仓库;开发机需要以服务账号权限验证时,可让已登录用户执行单条 gcloud ... --impersonate-service-account=<SERVICE_ACCOUNT_EMAIL>,取得短期凭据。客户端库需要 ADC 时可在受支持语言中使用 gcloud auth application-default login --impersonate-service-account=<SERVICE_ACCOUNT_EMAIL>,但应先确认该语言认证库支持 impersonation ADC。云上工作负载使用附加服务账号,外部 CI 使用 Workload Identity Federation。
环境变量 CLOUDSDK_CORE_PROJECT、CLOUDSDK_COMPUTE_REGION 等会高于 configuration 属性,gcloud properties 文档给出了 CLOUDSDK_SECTION_PROPERTY 命名规则。排障时同时查看 gcloud config list 和相关环境变量。退出时先 gcloud auth revoke <GCP_USER_EMAIL>;若创建过本地 ADC,再执行 gcloud auth application-default revoke。删除命名 configuration 前必须先激活另一个 configuration;隔离目录可在撤销后整体删除。
结构化输出是脚本契约,不是显示偏好
终端给人看可以用 table,脚本必须消费 JSON 或显式投影后的标量。三套 CLI 都支持服务端过滤与客户端格式化,但语义不同。AWS 的 --query 使用 JMESPath,且 text 输出在分页时可能逐页应用查询;Azure 默认 JSON,并用 JMESPath --query,官方输出格式说明提醒 TSV 字段顺序要通过显式投影固定;gcloud 使用自己的 --filter 与 --format 表达式,脚本指南还指出并行执行多个 gcloud 命令不受支持。
$ErrorActionPreference = 'Stop'
# AWS:保留 JSON,再由 PowerShell 解析
$awsIdentity = aws sts get-caller-identity `
--profile <AWS_PROFILE_DEV> --region <AWS_REGION> `
--output json --no-cli-pager | ConvertFrom-Json
if ($awsIdentity.Account -ne '<AWS_ACCOUNT_ID>') { throw 'AWS_ACCOUNT_MISMATCH' }
# Azure:完整比较 tenant 与 subscription
$azIdentity = az account show --subscription <AZURE_SUBSCRIPTION_ID> `
--output json | ConvertFrom-Json
if ($azIdentity.tenantId -ne '<AZURE_TENANT_ID>' -or
$azIdentity.id -ne '<AZURE_SUBSCRIPTION_ID>') { throw 'AZURE_SCOPE_MISMATCH' }
# Google Cloud:同时证明活动账号和目标项目;项目存在本身不能证明调用者正确。
$gcpIdentity = gcloud auth list `
--configuration=<GCP_CONFIG_DEV> `
--filter=status:ACTIVE `
--format='json(account,status)' | ConvertFrom-Json
if (@($gcpIdentity).Count -ne 1 -or $gcpIdentity[0].account -ne '<GCP_USER_EMAIL>') {
throw 'GCP_IDENTITY_MISMATCH'
}
$gcpProject = gcloud projects describe <GCP_PROJECT_ID> `
--configuration=<GCP_CONFIG_DEV> `
--project=<GCP_PROJECT_ID> `
--format='json(projectId,projectNumber,lifecycleState)' | ConvertFrom-Json
if ($gcpProject.projectId -ne '<GCP_PROJECT_ID>') { throw 'GCP_PROJECT_MISMATCH' }
Write-Output 'IDENTITY_GUARD_OK'这是正向实验:所有值与允许清单一致时输出 IDENTITY_GUARD_OK。反向实验只需把允许的 AWS account、Azure subscription 或 GCP project 中任意一个替换为 <INTENTIONALLY_WRONG_ID>,脚本必须在第一条变更命令之前非零退出,并留下稳定错误标签。不要把“查询返回空数组”当成作用域错误,也不要在身份不匹配后自动搜索其他账号直到找到同名资源。
结构化输出仍可能含敏感字段。资源 JSON 可能带私有 IP、内部域名、principal、标签中的工单号、临时 URL 或用户数据。CI 只保存白名单投影;变更命令若不需要 stdout,使用 Azure 的 --output none 或在确认无错误后丢弃普通输出,但 stderr、退出码和云端 request ID 仍要留作证据。禁止把原始 --debug 输出、完整环境变量和登录缓存作为 artifact。
项目脚本把身份护栏放在资源操作之前
仓库不要保存个人 profile,也不要在 .env 中放 access key、client secret 或服务账号 key。可以提交不含敏感值的运行契约,例如 cloud-targets.json 保存允许的账号、订阅、项目、区域和必需标签;真实身份由 SSO、workload identity 或 runner 元数据取得。脚本启动后先解析契约,再查询身份,最后才查询资源。
一条可审查的读操作链建议固定为:打印 CLI 与脚本版本;打印经脱敏的目标环境;检查配置隔离目录;查询调用者;比较不可变 ID;检查 region/zone;执行带 owner、environment、expiry 标签过滤的只读 inventory;保存白名单字段和退出码。写操作再增加计划文件、审批引用、幂等标识和二次读取。删除操作必须要求完整资源 ID、标签所有权和显式确认,禁止仅按名称模糊匹配。
不要用同一个跨云 shell 函数吞掉三家错误。AWS 403、Azure AuthorizationFailed、Google PERMISSION_DENIED 表面相似,背后的 principal、策略层级、资源标识和 request ID 结构不同。统一编排层只负责阶段、超时、退出码和审计摘要;每家 provider adapter 保留原始 JSON schema、错误分类和重试判断。API 返回 429 或服务端 5xx 可以按 Retry-After 与抖动退避重试,身份不匹配、403、参数错误和资源不存在通常不应盲目重试。
CLI 适合人工诊断、薄脚本和低频运维入口。需要复杂依赖图、幂等状态和审阅计划时,优先使用 Terraform、Bicep、CloudFormation 或对应 IaC;需要高吞吐数据面调用、稳定类型和应用内重试时,使用 SDK;需要一次性确认调用者与资源状态时,CLI 更直接。把几百行 CLI 脚本当成跨云控制平面,会逐渐复制出状态管理、漂移检测、锁、重试和回滚,却没有成熟 IaC 的可见性。
CI 用联邦短期身份,不把个人密钥搬进 Secret
人类登录和机器登录要分开。AWS 可让外部 OIDC principal 换取绑定 IAM role 的临时凭据,AWS OIDC 联邦说明明确建议避免在外部应用长期保存 AWS 凭据;Azure GitHub Actions 可在 Entra 应用或用户分配托管身份上配置 federated identity credential,Azure OIDC 指南要求工作流取得 ID token;Google Cloud 的部署流水线 Workload Identity Federation让外部 OIDC 身份取得短期 Google 凭据,免去服务账号 key 的维护。
permissions:
contents: read
id-token: write
jobs:
inventory:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<PINNED_COMMIT_SHA>
# 三者按实际 job 拆开;这里展示各自身份入口,不应同时授予一个 job 全部权限。
- uses: aws-actions/configure-aws-credentials@<PINNED_COMMIT_SHA>
with:
role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/<AWS_CI_ROLE>
aws-region: <AWS_REGION>
- uses: azure/login@<PINNED_COMMIT_SHA>
with:
client-id: <AZURE_CLIENT_ID>
tenant-id: <AZURE_TENANT_ID>
subscription-id: <AZURE_SUBSCRIPTION_ID>
- uses: google-github-actions/auth@<PINNED_COMMIT_SHA>
with:
workload_identity_provider: projects/<GCP_PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>
service_account: <GCP_SERVICE_ACCOUNT_EMAIL>
- run: ./scripts/cloud-identity-guard.ps1
shell: pwsh示例中的 action 必须固定到团队审查过的 commit SHA,升级由依赖机器人发起。实际流水线按云和职责拆 job:只读 inventory role 不应拥有创建或删除权限,部署 role 不应被 fork pull request、任意分支或任意 environment 获取。OIDC 信任条件至少绑定仓库不可变标识、分支或 environment、audience 与 subject;仅信任整个组织会扩大冒用面。id-token: write 只允许 job 请求 OIDC token,不等于自动获得云权限,真正权限由云端信任策略和 role assignment 决定。
失败证据要能区分三个阶段:CI 未取得 OIDC token;云端拒绝 token 的 issuer、audience、subject 或时间;联邦成功但角色无权访问目标资源。第一阶段查工作流 permission,第二阶段查 trust/federated credential 和 token claims,第三阶段查 IAM/RBAC 与资源 ID。不要遇到 403 就创建 client secret 作为“临时兜底”,它通常会永久留在 Secret 库里。
权限、敏感数据与审计必须同时设计
身份验证成功只证明“你是谁”,不证明“你该做什么”。人类开发者用只读或开发者 permission set/role,临时提权经过审批并缩短会话;CI 每个工作流用独立 workload identity,权限绑定到明确资源和动作;云上应用使用平台原生工作负载身份。管理员、Owner 和项目 Editor 不应成为普通 CLI 教程的默认角色。
最小权限实验要同时有正反两条。安全团队先在测试账号准备两个不含敏感数据的只读探针:允许角色可读取 <ALLOWED_RESOURCE_ID>,同一角色无权读取 <DENIED_RESOURCE_ID>。AWS 可对受限测试 S3 bucket 执行 aws s3api get-bucket-location --bucket <DENIED_BUCKET> --profile <AWS_PROFILE_DEV>;Azure 可执行 az resource show --ids <DENIED_RESOURCE_ID> --subscription <AZURE_SUBSCRIPTION_ID>;Google Cloud 可对另一个拒绝项目执行 gcloud projects describe <DENIED_PROJECT_ID> --configuration=<GCP_CONFIG_DEV> --project=<DENIED_PROJECT_ID>。正向命令必须返回预期对象,反向命令必须非零退出并分别留下 AccessDenied、AuthorizationFailed 或 PERMISSION_DENIED 一类服务端证据;若返回对象或仅得到空列表,门禁都应失败。不要用真正创建、删除、改 IAM、读取对象内容或访问生产资源来证明拒绝。拒绝记录只保留 principal、目标完整资源 ID、动作、错误码、request/correlation ID 和策略版本,凭据值保持遮罩。
敏感面不止 AK/SK。AWS SSO cache、STS session token,Azure MSAL token cache、client certificate,Google ADC、external account credential file、服务账号 impersonation token 都能在有效期内被滥用。内部域名、账号 ID、tenant ID、subscription ID、project ID 虽不一定是 secret,也属于应最小披露的基础设施元数据。shell history、进程参数、CI 日志、调试包、终端录屏和工单附件都要纳入泄漏模型。
审计关联不能依赖“某人说自己运行过”。每次脚本生成 <RUN_ID>,保存代码 commit、CLI 版本、脱敏 principal、账号/订阅/project、region/zone、命令类别、开始与结束相对时间、退出码和云端 request ID。云端审计日志再用 principal、资源 ID、动作与 request ID 反查。CLI 本地日志只能解释客户端解析,不能替代 CloudTrail、Azure Activity Log 或 Cloud Audit Logs 的服务端记录。
容量与费用从一次 list 命令就开始
CLI 安装本身不是云资源预算。真正费用来自命令触发的资源、API、日志、对象存储、快照、网络流量与 CI 时长。创建一台“只测试几分钟”的实例,可能同时留下磁盘、公网 IP、快照和日志;删除主资源不等于关联费用归零。执行任何创建示例前,应在 AWS Pricing Calculator、Azure Pricing Calculator 或 Google Cloud Pricing Calculator中按目标 region、SKU、时长和附属资源估算,并设置 owner、environment、cost-center、expiry 标签。
只读 inventory 也有容量代价。AWS CLI 默认会跟随服务分页继续请求,AWS 分页说明指出大列表可能在后台产生多次 API 调用;Azure 与 Google Cloud 服务同样可能分页、限流或受 API quota 约束。客户端 --query 或格式化通常发生在响应返回后,不能替代服务端过滤。先按 project、resource group、region 与标签缩小服务端结果,再投影字段;不要每分钟扫描组织下所有账号后在本地筛选。
容量治理至少观察:每次 inventory 的 API 调用数与耗时、分页数量、429/5xx 比例、返回字节、审计日志增量、CI 分钟数、凭据获取失败率和未清理资源数。轮询频率根据资源变化速度和服务配额确定,不能把 while true 加 sleep 当监控系统。高频资产同步应使用 Resource Graph、Cloud Asset Inventory、AWS Config/Resource Explorer 等专用索引能力并核对其费用与延迟,CLI 保留抽查和故障诊断入口。
从错误证据反推故障层
命令不存在时先查安装与 PATH;版本能输出但 help 异常,查扩展、组件和安装完整性。TLS、代理和 DNS 错误发生在认证之前:保留目标官方域名、代理路径和证书颁发者,修复企业根证书信任,不要全局关闭证书校验。AWS 签名、OIDC 和 OAuth 都依赖时间,虚拟机或容器时钟漂移会表现为 token 尚未生效、已过期或签名不匹配。
认证失败时区分“没有凭据”“凭据过期”“身份提供者拒绝”“token 交换失败”。AWS 用 aws configure list 看来源,再刷新 SSO;Azure 核对 tenant、条件访问与 az account list;gcloud 分清 gcloud auth 和 ADC,分别检查活动账号和 ADC 配置来源。不要把 gcloud auth application-default print-access-token 当作常规排障命令:它会把 bearer token 直接写到标准输出,极易被终端记录、重定向或 CI 日志收集。
授权失败说明认证大概率已经完成。记录 principal、动作、资源完整 ID 和错误码,检查策略绑定层级、显式 deny、条件、permission boundary 或组织策略。刚加权限后不要无限重试;先确认变更是否作用到同一个 principal,等待平台传播窗口,再重复同一条只读命令。把角色升到管理员只会让现象消失,无法证明最小权限配置正确。
资源“查不到”要沿容器和位置检查:AWS account、partition、region;Azure cloud、tenant、subscription、resource group、location;Google account、project、region/zone。空列表不是网络错误,404 也不总是不存在,有些服务会用 404 隐藏无权访问的对象。用控制台交叉检查时仍需确认控制台当前身份,不要把另一个登录会话当真相。
脚本偶发失败再看分页、限流、并发与进程状态。gcloud 官方不支持多个命令并行执行,多个进程还可能争用配置文件;Azure 多终端共享活动 subscription 会互相改变默认值;AWS 环境变量能让一个子进程绕过 profile。并发任务使用独立配置目录、显式作用域和有上限的退避,结果按 <RUN_ID> 分目录,避免后完成的 job 覆盖前一个证据。
清理、退出与回滚要覆盖本地和云端
只读实验没有创建云资源,清理重点是退出、缓存和临时配置。下面命令应在确认隔离目录后执行;不要递归删除默认 home 下的共享配置。
# 先撤销或退出会话
aws sso logout
az logout
az account clear
gcloud auth revoke <GCP_USER_EMAIL> --quiet
gcloud auth application-default revoke --quiet
# 只有路径确实是本次 RUN_ID 的隔离目录时才删除
$resolved = (Resolve-Path -LiteralPath $sandbox).Path
$tempRoot = (Resolve-Path -LiteralPath $env:TEMP).Path
if (-not $resolved.StartsWith($tempRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
$resolved -notlike '*cloud-cli-isolation-<RUN_ID>*') {
throw 'REFUSE_TO_DELETE_NON_ISOLATED_CONFIG'
}
Remove-Item -LiteralPath $resolved -Recurse -Force若实验创建过资源,退出顺序是先读取完整资源 ID 与标签,确认 owner 和依赖,导出必要数据,执行经审批的删除,再以同一账号、订阅、项目和 region/zone 查询不存在,最后在费用与审计视图复核。磁盘、快照、IP、对象版本、密钥、role assignment、日志与预算要分别检查;主资源删除成功不能替代这些证据。删除命令不应直接出现在通用教程脚本中,团队应为每类资源维护带保护条件的回收 runbook。
回滚 CLI 升级时恢复客户端、扩展/组件和 runner 镜像三者,重新执行隔离配置、身份正向、身份错配反向、只读 inventory 与登出实验。回滚认证改造时不能恢复个人长期密钥;应恢复上一版联邦信任策略或禁用新 job,并验证旧角色仍受最小权限约束。彻底退出某云时,还要撤销 OIDC provider/federated credential、删除 workload identity、role assignment 和失效 Secret,关闭 CI 权限,保留必要审计后按保留策略到期删除。
长期治理让每条命令都有 owner
平台团队维护 CLI 版本基线、runner 镜像、身份护栏库和错误分类;云安全团队维护 SSO、OIDC 信任、permission set/RBAC/IAM 与审计策略;应用团队拥有目标账号、subscription、project、region、标签和资源生命周期。职责交界用机器可读契约表达,不能靠 Wiki 上一句“请确认环境”。
每个季度或重大升级后演练四件事:正确身份能完成只读查询;故意错误的账号或项目在变更前被脚本拒绝;过期会话能给出可识别错误并安全刷新;撤销 workload identity 后 CI 确实无法访问。再抽查配置目录是否混入长期密钥、流水线是否仍保存 client secret、默认 subscription/project 是否被脚本依赖、离职人员和废弃仓库是否仍在联邦信任中。
治理指标不只看命令成功率。还要看身份错配被拦截次数、长期凭据数量、短期会话平均时长、未标记资源、过期资源、429 与分页趋势、CI 身份失败分类、审计关联成功率和清理残留。任何自动化若不能在执行前回答“谁、在哪个资源容器、哪个位置、以什么权限、将操作哪些完整资源 ID”,就还没有资格获得写权限。
成熟的三云 CLI 工作流并不追求把三家命令伪装成一样,而是承认它们的身份和资源模型不同,在共同入口设置同样严格的证据门:版本可追溯、配置彼此隔离、身份用不可变 ID 证明、位置显式、输出结构化、CI 使用短期联邦身份、失败可分层、缓存可撤销、资源和费用可清理。做到这些,CLI 才从个人终端里的便利工具,变成团队可以审查、复现和长期维护的工程能力。
