Safely Shrink IAM Policies with Terraform and IAM Shrink
It’s a common security practice to require that IAM policies are explicit. Meaning they don’t allow any wildcards in IAM actions. This is a good standard and should be followed whenever possible. There are also limits to how big an IAM policy can be. Normally for IAM managed policies you can split them up into multiple policies and apply them. For resource policies, you can’t do that. If you are sharing resources such as S3 Buckets or KMS Keys across accounts those permissions must all fit in a single S3 Bucket or KMS Key policy. At scale across many organizations this can be a challenge to keep within the limits. Similar limits exists with Service Control Policies (SCPs).
Commonly what you do is add wildcards to the list of actions in the policy. This is error prone for two reasons:
- Manually adding wildcards to a list of IAM actions can easily result in a policy that allows more than you intended.
- If AWS adds a new action that matches one of your wildcards, there is no simple way to exclude the new action.
IAM Shrink can help with both of these.
How IAM Shrink Works
IAM Shrink takes a list of actions and adds wildcards *
to the policy to reduce the list of actions. Think of it as a gzip for IAM policies.
It guarantees that only the IAM actions you specify will be matched by the new list of patterns.
IAM Actions are camel cased into a number of words. For example:
s3:GetObject
-> “Get” “Object”s3:GetObjectTagging
-> “Get” “Object” “Tagging”
IAM Shrink automatically corrects capilization and only replaces only whole words. So s3:GetObject
will never get shrunk to something like s3:*et*
.
Shrinking is configurable so you can choose your best combination of final size vs. readability.
It’s backed by iam-data which is updated daily through iam-collect. So it’s dead simple to keep up to date with the latest actions.
How To Use IAM Shrink
Use on the Web
The easist way to see it in action is to use the IAM Shrink Page. You can paste in a list of actions or a full policy document and try it out. Play with the number of number of iterations to see how it affects the output.
Use the CLI
The second way is to install the npm package which includes a CLI. You can pass in a space separated set of actions to the command or pipe in a list of strings or a policy document.
Integrate the CLI into your Terraform Plan
You can make a Terraform module to shrink your IAM policies directly in your Terraform plan.
Create a shrink_actions
module with these files:
shrink.tf
shrink.sh
shrink.tf
has a simple external data source that calls the shrink.sh
script and makes the results available. Terraform’s external
only allows string inputs and outputs, so there is a little string joining and splitting to get an array in and out of the output.
variable "actions" {
type = list(string)
description = "The list of IAM actions to shrink."
}
variable "iterations" {
type = number
description = "The number of shrink iterations to run against the list of actions. The higher the number, the smaller the final list. 0 or less do as many iterations as possible."
default = 2
}
data "external" "shrinked_actions" {
program = flatten(["shrink_actions/shrink.sh", "--iterations=${var.iterations}", var.actions])
}
output "shrunk_actions" {
value = split(",", data.external.shrinked_actions.result.actions)
}
shrink.sh
is a simple bash script that calls the IAM Shrink CLI with the list of actions passed in as arguments.
#!/bin/bash
# Store all script arguments in an array
args=("$@")
# Pass all the arguments staight to iam-shrink
output=$(/usr/bin/iam-shrink "${args[@]}")
# Check for errors
status=$?
if [ $status -ne 0 ]; then
echo "{\"error\": \"Failed to run iam-shrink\"}"
exit $status
fi
# Join the individual lines of output with commas
comma_delimited=$(echo "$output" | tr '\n' ',' | sed 's/,$//')
# Create a JSON document with the key "actions"
echo "{\"actions\": \"$comma_delimited\"}"
Here is an example of using the module:
module "shrunk_key_actions" {
source = "./shrink_actions"
iterations = 0
actions = [
"kms:CancelKeyDeletion",
"kms:ConnectCustomKeyStore",
"kms:CreateAlias",
"kms:CreateCustomKeyStore",
"kms:CreateGrant",
"kms:CreateKey",
"kms:Decrypt",
"kms:DeleteAlias",
"kms:DeleteCustomKeyStore",
"kms:DeleteImportedKeyMaterial",
"kms:DeriveSharedSecret",
"kms:DescribeCustomKeyStores",
"kms:DescribeKey",
"kms:DisableKey",
"kms:DisconnectCustomKeyStore",
"kms:EnableKey",
"kms:EnableKeyRotation",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:GenerateDataKeyPair",
"kms:GenerateDataKeyPairWithoutPlaintext",
"kms:GenerateDataKeyWithoutPlaintext",
"kms:GenerateMac",
"kms:GenerateRandom",
"kms:GetKeyPolicy",
"kms:GetKeyRotationStatus",
"kms:GetParametersForImport",
"kms:GetPublicKey",
"kms:ImportKeyMaterial",
"kms:ListAliases",
"kms:ListGrants",
"kms:ListKeyPolicies",
"kms:ListKeyRotations",
"kms:ListKeys",
"kms:ListResourceTags",
"kms:ListRetirableGrants",
"kms:PutKeyPolicy",
"kms:ReEncryptFrom",
"kms:ReEncryptTo",
"kms:ReplicateKey",
"kms:RetireGrant",
"kms:RevokeGrant",
"kms:RotateKeyOnDemand",
"kms:ScheduleKeyDeletion",
"kms:Sign",
"kms:SynchronizeMultiRegionKey",
"kms:TagResource",
"kms:UntagResource",
"kms:UpdateAlias",
"kms:UpdateCustomKeyStore",
"kms:UpdateKeyDescription",
"kms:UpdatePrimaryRegion",
"kms:Verify",
"kms:VerifyMac"
]
}
data "aws_iam_policy_document" "key_policy" {
statement {
# Access for the key owner
}
statement {
sid = "CrossAccountAccess"
actions = module.shrunk_key_actions.shrunk_actions
effect = "Allow"
resources = ["*"]
principals {
type = "AWS"
identifiers = ["cross account role arn"]
}
}
}
resource "aws_kms_key" "key" {
description = "This key is used to encrypt and decrypt secrets"
policy = data.aws_iam_policy_document.key_policy
}
output "shrunk_actions" {
value = module.shrunk_key_actions.shrunk_actions
}
This will shrink the list of actions in the CrossAccountAccess
statement to the smallest list of patterns that avoids all actions not in the original list.
For example here is the ouput of the module:
Changes to Outputs:
+ shrunk_actions = [
+ "kms:Disconnect*",
+ "kms:Schedule*",
+ "kms:Describe*",
+ "kms:Generate*",
+ "kms:*Resource",
+ "kms:Connect*",
+ "kms:Rotate*",
+ "kms:Enable*",
+ "kms:Cancel*",
+ "kms:Derive*",
+ "kms:Delete*",
+ "kms:Import*",
+ "kms:Create*",
+ "kms:Update*",
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:*Alias",
+ "kms:*Grant",
+ "kms:Verify",
+ "kms:List*",
+ "kms:Put*",
+ "kms:*Mac",
+ "kms:Get*",
+ "kms:*Key",
+ "kms:Sign",
+ "kms:Re*",
]
To integrate this in your pipeline you just need a couple extra lines in your build script:
# Install iam-shrink
npm install -g @cloud-copilot/iam-shrink
# Update iam-data in case it was already installed to get the latest actions
npm update -g @cloud-copilot/iam-data
# Then run the rest of your pipeline as normal
terraform plan
So now you have a fully automated way to shrink your IAM policies in your Terraform code!
Benefits
- Avoid accidentally including unintended actions in your policy.
- Automatically update your wildcards in your final policy when AWS adds new actions. Just re-run the pipeline!
- Keep the exact list of actions in your Terraform code. Your intentions are clear and will be well defined for history and review.