The IAM policy

The IAM policy Kulshan runs with.

The composed policy contains 160 unique IAM actions and grants no permission to create, update, modify, or delete customer resources. Verify the hash, inspect the JSON, and decide whether the access boundary fits your environment.

policy version
0.4.2 (released 2026-07-20), unchanged since. No new IAM actions have been required by any Kulshan release after 0.4.2, including the current 0.6.2. The policy version and the CLI version track separately: this number is not the CLI falling behind, it is the policy having nothing new to grant.
total actions
160
aws services
33
sha256
6533e48168c2a4ce8e96f383f42534a9f05575ad459f1e5b9f86dcbac632678f
file
Kulshan-readonly.json (composed policy, 7.1 KB)
source
Generated from per-pack policies in Kulshan/iam/per-check/. Composed at release time, attested by hash.

Current public PyPI release: Kulshan 0.6.2. Kulshan master includes the IAM registry/reference alignment fix in commit 110a919, which did not add or remove any IAM action. SHA-256: 6533e48168c2a4ce8e96f383f42534a9f05575ad459f1e5b9f86dcbac632678f.

The policy contains 160 unique IAM actions and grants no permission to create, update, modify, or delete customer resources. Every action is classified non-write in the AWS Service Authorization Reference snapshot dated 2026-08-19.

Three actions do not use Get, List, or Describe naming: cloudformation:DetectStackDrift, iam:GenerateCredentialReport, and iam:GenerateServiceLastAccessedDetails. All three are classified non-write by AWS in that snapshot.

What the policy allows.

Plain-language summary. Every line below maps to read-only AWS APIs. The full action list is in the JSON file above.

spend
Cost Explorer queries (cost and usage, anomaly history, RI/SP coverage and recommendations, forecast). Aggregate cost data only. ce:Get*
compute
EC2 instances, EBS volumes and snapshots, security groups, route tables, NAT gateways, Auto Scaling groups, Lambda function configuration. ec2:Describe*, autoscaling:Describe*, lambda:List*, lambda:GetFunction*
containers
ECS clusters and services, EKS clusters, ECR repositories, image scan findings. Configuration only.
storage
S3 bucket inventory, encryption, versioning, public-access settings, lifecycle policies, replication. Bucket configuration only, no object data.
database
RDS clusters and instances, parameter groups, snapshots, DynamoDB tables, ElastiCache clusters. Configuration only.
network
VPC topology, subnets, route tables, peering, transit gateways, ELB / ALB / NLB, Route 53 hosted zones. Configuration only.
identity
IAM users, roles, groups, policies (their configuration, not their secrets), Access Analyzer findings, organization structure, caller identity. iam:Get*, iam:List*, access-analyzer:Get*, access-analyzer:List*, organizations:Describe*, sts:GetCallerIdentity
audit log
CloudTrail trail configuration and integrity validation status, AWS Config recorders and rules, X-Ray traces. Whether logging is on, not the log contents.
monitoring
CloudWatch metrics, alarms, dashboards, log-group existence and retention. Log-group names and retention, not the log lines.
encryption
KMS key configuration (rotation, policy, grants). No Decrypt. No GenerateDataKey. No cryptographic operations.
backup
AWS Backup vaults, plans, recovery points, protected resources. Configuration and inventory only.
threat data
GuardDuty detector configuration and findings.
tags
Resource Groups Tagging API queries to compute tag coverage and ownership gaps.
capacity
Service Quotas (current usage and limits), CloudFormation stack drift detection status.
delivery
SNS topic configuration. Used by the observability pack to map alarm-to-channel wiring.

What it explicitly does NOT allow.

Verbs the policy never grants. If you find a Kulshan code path that calls one of these, that is a bug, and it should be reported to [email protected].

  • No Put, Create, Update, Modify, Delete, or Terminate on any service.
  • No iam:CreateAccessKey, iam:UpdateLoginProfile, iam:CreateUser, or any IAM mutation.
  • No sts:AssumeRole in the policy itself. Kulshan uses your existing credential chain; assuming a role is your decision, not the policy's.
  • No s3:GetObject. The bucket configuration is readable; the bucket contents are not.
  • No kms:Decrypt, kms:GenerateDataKey, kms:Encrypt, or any cryptographic operation.
  • No ec2:RunInstances, ec2:TerminateInstances, ec2:CreateTags, or any compute-lifecycle action.
  • No rds:Modify*, lambda:UpdateFunctionCode, secretsmanager:GetSecretValue, or any other resource modification or secret access.

Verify it yourself.

Download the JSON, hash it locally, and compare to the SHA256 above. If the two match, the file you have is the file I ship.

$ shasum -a 256 Kulshan-readonly.json6533e48168c2a4ce8e96f383f42534a9f05575ad459f1e5b9f86dcbac632678f Kulshan-readonly.json

If the hash does not match, something is wrong. Email [email protected].

Per-pack policies.

Each pack ships its own minimum-viable policy. Attach only the packs you intend to run. Per-pack totals do not add up to 160 because some IAM actions are reused across multiple checks. The composed policy de-duplicates shared actions into 160 unique IAM actions.

Pack
Actions
Services
Download
age
16
7
core
2
2
cost
15
3
dr
29
10
drift
12
6
limit
19
10
pulse
23
14
security
81
14
sweep
24
9
tag
33
11
topo
14
2

Trust Ledger.

Corrections, disclosures, and integrity events. Newest first.

2026-08-23 // v0.6.2

0.6.2 is published on PyPI as of 2026-08-23. This entry disclosed a fix that was committed before it was released; the fix is now installable.

The residual identifier gate (Gate 3), the last automated check before a pseudonymized consultant evidence package leaves the customer's environment, could silently pass three classes of real identifiers instead of blocking them: AWS account IDs beginning with "0", account IDs ending in "000000", and public IPv4 addresses.

This is the same defect class as the 2026-07-20 entry below: a check reporting clean when it should report "could not verify." A consultant evidence package is built specifically to leave the customer's environment, so a gate that can be silently bypassed on ordinary, real-shaped data is a leak path, not a cosmetic bug.

I removed the value-based skip list from the account-ID check entirely; every 12-digit match is now a failure, with no exceptions for values that merely look like timestamps or round numbers. The IPv4 check now validates every octet and covers public addresses as well as private ones, not just RFC1918 ranges. Both fixes are pinned by an expanded test suite covering the exact bypasses found.

fixed version
0.6.2 (published 2026-08-23)
affected versions
0.6.0 and 0.6.1
action required
Upgrade to 0.6.2 and regenerate any consultant evidence package produced with Kulshan 0.6.0 or 0.6.1.

2026-08-21 // v0.6.1

Kulshan 0.6.0's consultant evidence export could write real, unpseudonymized AWS account identifiers into the package's own manifest.json, even though the evidence data itself (the CUR Parquet) was correctly pseudonymized.

The manifest ships inside the same ZIP as the evidence and is the file a recipient is most likely to open first. A real account identifier sitting there defeats the point of pseudonymizing the data next to it.

Packages are now assembled and validated in temporary storage and only moved to the requested output path after every gate passes. The residual identifier gate scans every row of every package member, including manifest.json, privacy-report.json, and README.md, not just the evidence data, and a scan error now fails the export instead of letting it through.

fixed version
0.6.1
affected versions
0.6.0
action required
Regenerate any consultant evidence package produced with Kulshan 0.6.0.

2026-07-20 // v0.4.2

Three S3 entries in the published IAM policy used AWS API operation names instead of IAM action names. They granted nothing. More importantly, the scanner could treat the resulting authorization failure as a clean result, meaning a user applying the published least-privilege policy could receive a false clean for an affected S3 check.

I corrected the IAM action names and changed the scanner so a failed evaluation is reported as "could not check," with the denied action named. A check is now marked clean only when the required evidence was successfully retrieved and evaluated.

fixed version
0.4.2
affected versions
All versions prior to 0.4.2 (exact first-affected version could not be determined)
previous policy sha256
3c9e7673091705aa70be9990e81ee5625bc876e2a4d68748e3761d4233decc7b
corrected policy sha256
6533e48168c2a4ce8e96f383f42534a9f05575ad459f1e5b9f86dcbac632678f
incorrect actions
s3:GetBucketEncryption, s3:GetBucketLifecycleConfiguration, s3:GetBucketReplication
correct actions
s3:GetEncryptionConfiguration, s3:GetLifecycleConfiguration, s3:GetReplicationConfiguration
corrected policy

Reuse

Reuse freely under CC BY 4.0. Credit: MissionFinOps, missionfinops.com.

This applies to the policy file (kulshan-readonly.json and the per-pack files in kulshan/iam/per-check/) only. The CC BY 4.0 license on the policy file is in addition to the Apache 2.0 license that covers the rest of the Kulshan codebase.

If your team or tool adopts this policy as a baseline, no permission needed. A link back is appreciated, not required.