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.
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.
Decrypt. No GenerateDataKey. No cryptographic operations.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, orTerminateon any service. - No
iam:CreateAccessKey,iam:UpdateLoginProfile,iam:CreateUser, or any IAM mutation. - No
sts:AssumeRolein 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.
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.
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.
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.
s3:GetBucketEncryption, s3:GetBucketLifecycleConfiguration, s3:GetBucketReplications3:GetEncryptionConfiguration, s3:GetLifecycleConfiguration, s3:GetReplicationConfigurationReuse 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.