The rule
Access and continuity
You create the credentials: read-only, MFA, expiring on a date you set.
Boundary
The boundary
| You grant | Never received |
|---|---|
| Read on the billing exports | Object contents, rows, backups, secrets |
| Resource metadata: type, region, size, tags | pods/log, pods/exec, configmaps, secrets |
| Token and GPU metering | Prompt and completion payloads, embeddings |
| Repositories you invite us to | Default-branch write. Changes arrive as pull requests |
Continuity
Continuity
No named substitute engineer, and none claimed.
| Trigger | Automatically |
|---|---|
| From day one | Every artifact lives in your account |
| Unavailable 5 business days | Clocks pause, retainer months prorate |
| You terminate on it | Unearned fees refunded pro rata |
AWS
AWS policy
Data-plane reads denied, including bedrock:InvokeModel.
Trust policy: MFA, external ID, expiry
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeOnlyWithMfaExternalIdAndAnExpiry",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ENGINEER-ACCOUNT-ID:root" },
"Action": "sts:AssumeRole",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" },
"StringEquals": { "sts:ExternalId": "ONE-TIME-VALUE-YOU-GENERATE" },
"DateLessThan": { "aws:CurrentTime": "2026-09-30T00:00:00Z" }
}
}
]
}Permissions policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CurBucketRead",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetObject"],
"Resource": [
"arn:aws:s3:::YOUR-CUR-BUCKET",
"arn:aws:s3:::YOUR-CUR-BUCKET/cur2/*",
"arn:aws:s3:::YOUR-CUR-BUCKET/focus/*"
]
},
{
"Sid": "AthenaAgainstTheCurDatabaseOnly",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults",
"athena:GetWorkGroup",
"glue:GetDatabase",
"glue:GetTable",
"glue:GetTables",
"glue:GetPartitions"
],
"Resource": "*"
},
{
"Sid": "BillingRatesAndCommitmentPosture",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetCostAndUsageWithResources",
"ce:GetSavingsPlansUtilization",
"ce:GetSavingsPlansCoverage",
"ce:GetReservationUtilization",
"ce:GetReservationCoverage",
"savingsplans:DescribeSavingsPlans",
"pricing:GetProducts"
],
"Resource": "*"
},
{
"Sid": "TagAndResourceMetadataRead",
"Effect": "Allow",
"Action": [
"tag:GetResources",
"tag:GetTagKeys",
"tag:GetTagValues",
"organizations:ListAccounts",
"organizations:ListTagsForResource",
"cloudwatch:ListMetrics",
"cloudwatch:GetMetricData",
"compute-optimizer:GetEC2InstanceRecommendations",
"eks:ListClusters",
"eks:DescribeCluster",
"eks:ListNodegroups",
"eks:DescribeNodegroup",
"bedrock:ListFoundationModels",
"bedrock:GetModelInvocationLoggingConfiguration"
],
"Resource": "*"
},
{
"Sid": "DenyEveryDataPlaneReadIncludingModelCalls",
"Effect": "Deny",
"Action": [
"s3:GetObject",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"ssm:GetParameters",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
"rds-data:ExecuteStatement",
"kms:Decrypt",
"logs:GetLogEvents",
"logs:FilterLogEvents",
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:Converse",
"bedrock:ConverseStream"
],
"NotResource": [
"arn:aws:s3:::YOUR-CUR-BUCKET/cur2/*",
"arn:aws:s3:::YOUR-CUR-BUCKET/focus/*"
]
}
]
}Attach AWSBillingReadOnlyAccess alongside.
Clusters
Read-only cluster access
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cost-mechanic-read
rules:
# Capacity, requests and ownership. No pods/log, no pods/exec, no secrets.
- apiGroups: [""]
resources: ["nodes", "namespaces", "pods", "persistentvolumeclaims",
"resourcequotas", "limitranges", "services"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
verbs: ["get", "list"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list"]
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes", "pods"]
verbs: ["get", "list"]
- apiGroups: ["autoscaling", "karpenter.sh"]
resources: ["horizontalpodautoscalers", "nodepools", "nodeclaims"]
verbs: ["get", "list"]Azure
Azure roles, at the billing scope
# 1. Cost Management Reader at the billing scope, not the subscription.
# Unused reservation charges land on the billing profile, so subscription
# scope alone hides reservation waste.
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Cost Management Reader" \
--scope "/providers/Microsoft.Billing/billingAccounts/$BILLING_ACCOUNT"
# 2. Reader on the subscriptions in scope. Reader cannot write and cannot
# read data-plane contents.
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Reader" --scope "/subscriptions/$SUBSCRIPTION_ID"
# 3. Read on the container the Cost Management export writes to. Both
# datasets: amortized AND actual.
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG\
/providers/Microsoft.Storage/storageAccounts/$SA/blobServices/default\
/containers/$EXPORT_CONTAINER"
# 4. AKS: cluster user + RBAC Reader. Reader-only kubeconfig, never admin.
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Azure Kubernetes Service Cluster User Role" \
--scope "$AKS_RESOURCE_ID"
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Azure Kubernetes Service RBAC Reader" --scope "$AKS_RESOURCE_ID"
# 5. Azure OpenAI: metrics only. Monitoring Reader reads
# ProcessedPromptTokens and GeneratedTokens. It cannot call a deployment.
# Cognitive Services User is NOT requested and must not be granted.
az role assignment create \
--assignee-object-id "$OBJECT_ID" --assignee-principal-type User \
--role "Monitoring Reader" --scope "$AOAI_RESOURCE_ID"GCP
GCP bindings, time-bound
# A time-bound condition on every binding. Set the date to the last day of
# the engagement window; the binding stops working on its own.
COND='expression=request.time < timestamp("2026-09-30T00:00:00Z"),title=engagement-window'
# 1. Billing account viewer: rates, invoices, commitment inventory.
gcloud beta billing accounts add-iam-policy-binding "$BILLING_ACCOUNT" \
--member="user:$ENGINEER" --role="roles/billing.viewer"
# 2. Read on the detailed usage export dataset only, not the whole project.
bq add-iam-policy-binding \
--member="user:$ENGINEER" --role="roles/bigquery.dataViewer" \
"$PROJECT:billing_export"
# 3. Permission to run the queries, scoped by condition.
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$ENGINEER" --role="roles/bigquery.jobUser" --condition="$COND"
# 4. GKE, Vertex AI and metrics: viewer roles, no data-plane access.
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$ENGINEER" --role="roles/container.clusterViewer" --condition="$COND"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$ENGINEER" --role="roles/aiplatform.viewer" --condition="$COND"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$ENGINEER" --role="roles/monitoring.viewer" --condition="$COND"
# 5. Revoke, on the last day. Run this yourself and confirm it returned clean.
gcloud projects get-iam-policy "$PROJECT" \
--flatten="bindings[].members" --filter="bindings.members:$ENGINEER" \
--format="table(bindings.role)"Providers
Metering keys, not inference keys
SOC 2
SOC 2: none
None this year, either. If your review cannot waive it, we are the wrong fit.
Absent, not pending: SOC 2, ISO 27001, penetration test, insurance certificate, named substitute engineer.
The access appendix: grants by cloud, retention, insurance, compensating controls, capacity.
Next
Write the emailThe arithmeticTerms
Reviewer questions: hello@costmechanic.com.