A Terraform module for creating a least privilege access role for the Terraform S3 backend, including access to state files in S3 and locking with DynamoDB. Permissions can be further restricted to only those needed to run a plan
or read-only without locking.
At its simplest, this module can be used to generate a role with minimal permissions for a given backend configuration.
Example Configuration | Terraform Backend |
---|---|
module "access_role" {
source = "george-richardson/state-access-role/aws"
name = "state-access"
state_bucket_arn = "arn:aws:s3:::my-state-bucket"
lock_table_arn = "arn:aws:dynamodb:eu-west-1:123456123456:table/my-lock-table"
trust_policy = data.aws_iam_policy_document.trust.json
can_apply = [{
key = "terraform.tfstate"
}]
} |
terraform {
backend "s3" {
region = "eu-west-1"
bucket = "my-state-bucket"
dynamodb_table = "my-lock-table"
key = "terraform.tfstate"
assume_role = {
# Created by the module, output as role_arn
role_arn = "arn:aws:iam::123456123456:role/state-access"
}
}
} |
See the permissions policy generated from the above code.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:ListBucket",
"Condition": {
"StringLike": {
"s3:prefix": [
"env:/",
"terraform.tfstate"
]
}
},
"Effect": "Allow",
"Resource": "arn:aws:s3:::my-state-bucket",
"Sid": "S3List"
},
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::my-state-bucket/terraform.tfstate",
"arn:aws:s3:::my-state-bucket/env:/*/terraform.tfstate"
],
"Sid": "S3Write"
},
{
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Condition": {
"ForAllValues:StringLike": {
"dynamodb:LeadingKeys": [
"my-state-bucket/env:/*/terraform.tfstate",
"my-state-bucket/env:/*/terraform.tfstate-md5",
"my-state-bucket/terraform.tfstate",
"my-state-bucket/terraform.tfstate-md5"
]
}
},
"Effect": "Allow",
"Resource": "arn:aws:dynamodb:eu-west-1:123456123456:table/my-lock-table",
"Sid": "DynamoWrite"
}
]
}
The above configuration would grant the ability to read, write and lock state when using any workspace and the key terraform.tfstate
.
Warning
You should always review the permissions generated by this module.
Permissions can be restricted using the can_plan
and can_read
variants, or restricting workspaces as documented below.
This module is most useful when used to grant access to the centralised state of multiple projects with multiple workspaces. e.g.
module "access_role" {
source = "george-richardson/state-access-role/aws"
name = "developer-state-access"
state_bucket_arn = "arn:aws:s3:::my-state-bucket"
lock_table_arn = "arn:aws:dynamodb:eu-west-1:123456123456:table/my-lock-table"
trust_policy = data.aws_iam_policy_document.trust.json
can_apply = [
# Grant access to run apply actions using the "dev" workspace of any
# configuration with a workspace_key_prefix matching "applications/my-app"
{
key = "terraform.tfstate"
workspace_key_prefix = "applications/my-app"
workspaces = ["dev"]
# Prevent use of default workspace (i.e. S3 key without workspace_key_prefix).
allow_default_workspace = false
},
# Grant access to run apply actions using any key prefixed with "sandbox/*"
{
key = "sandbox/*"
# Do not allow workspace use (unless matching above key)
workspaces = []
}
]
can_plan = [
# Grant access to run plan actions against the "test" and "staging"
# workspaces of configurations with a workspace_key_prefix matching
# "applications/my-app"
# can_plan allows read access to state and the ability to take out locks
{
key = "terraform.tfstate"
workspace_key_prefix = "applications/my-app"
workspaces = ["test", "staging"]
allow_default_workspace = false
}
]
can_read = [
# Grant access to read state of all workspaces of the configuration
# with a workspace_key_prefix of "infra/network". This does not allow
# locking.
# For example application configurations may pull "infra/network" state
# using a terraform_remote_state data source.
{
key = "terraform.tfstate"
workspace_key_prefix = "infra/network"
}
]
}
See the permissions policy generated from the above code.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:ListBucket",
"Condition": {
"StringLike": {
"s3:prefix": [
"applications/my-app/",
"infra/network/",
"sandbox/*",
"terraform.tfstate"
]
}
},
"Effect": "Allow",
"Resource": "arn:aws:s3:::my-state-bucket",
"Sid": "S3List"
},
{
"Action": "s3:GetObject",
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::my-state-bucket/terraform.tfstate",
"arn:aws:s3:::my-state-bucket/infra/network/*/terraform.tfstate",
"arn:aws:s3:::my-state-bucket/applications/my-app/test/terraform.tfstate",
"arn:aws:s3:::my-state-bucket/applications/my-app/staging/terraform.tfstate"
],
"Sid": "S3Read"
},
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::my-state-bucket/sandbox/*",
"arn:aws:s3:::my-state-bucket/applications/my-app/dev/terraform.tfstate"
],
"Sid": "S3Write"
},
{
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Condition": {
"ForAllValues:StringLike": {
"dynamodb:LeadingKeys": [
"my-state-bucket/applications/my-app/dev/terraform.tfstate",
"my-state-bucket/applications/my-app/dev/terraform.tfstate-md5",
"my-state-bucket/applications/my-app/staging/terraform.tfstate",
"my-state-bucket/applications/my-app/staging/terraform.tfstate-md5",
"my-state-bucket/applications/my-app/test/terraform.tfstate",
"my-state-bucket/applications/my-app/test/terraform.tfstate-md5",
"my-state-bucket/sandbox/*",
"my-state-bucket/sandbox/*-md5"
]
}
},
"Effect": "Allow",
"Resource": "arn:aws:dynamodb:eu-west-1:123456123456:table/my-lock-table",
"Sid": "DynamoWrite"
}
]
}
You can pass in state_bucket_kms_key_arn
and lock_table_kms_key_arn
to grant least privilege permissions for underlying KMS keys.
If you'd like to add minimal permissions to an existing role, use the permissions-policy submodule.
module "state_access_policy" {
source = "george-richardson/state-access-role/aws//modules/permissions-policy"
state_bucket_arn = "arn:aws:s3:::my-state-bucket"
lock_table_arn = "arn:aws:dynamodb:eu-west-1:123456123456:table/my-lock-table"
can_apply = [{
key = "terraform.tfstate"
}]
}
resource "aws_iam_role_policy" "state_access" {
name = "state-access"
role = "my-role"
policy = module.state_access_policy.json
}
Name | Version |
---|---|
terraform | >= 1.2.0 |
aws | >= 4.9.0 |
Name | Version |
---|---|
aws | >= 4.9.0 |
Name | Source | Version |
---|---|---|
permissions_policy | ./modules/permissions-policy | n/a |
Name | Type |
---|---|
aws_iam_role.this | resource |
aws_iam_role_policy.inline_policy | resource |
aws_iam_policy_document.trust | data source |
Name | Description | Type | Default | Required |
---|---|---|---|---|
allow_full_bucket_list | Whether to allow the role to list all objects in the state bucket rather than just those needed by workspace definitions. This can be useful for reducing the size of the generated policy. | bool |
false |
no |
can_apply | Backend configurations that can be applied. This allows locking of state and writing to the state bucket. Fields: (Required) key: Path to the state file inside the S3 Bucket. Supports wildcards. (Optional) workspace_key_prefix: Prefix applied to the state path inside the bucket when using workspaces. Supports wildcards. Default: "env:" (Optional) workspaces: List of workspaces that can be accessed. Supports wildcards. Default: ["*"] (Optional) allow_default_workspace: Allow access to the default workspace (i.e. the value of key with no workspace_key_prefix). Default: true |
list(object({ |
[] |
no |
can_plan | Backend configurations that can be planned. State locking is allowed here for use in plans, however write access to the state bucket is prevented. WARNING: principals will be able to start apply runs using these permissions, but won't be able to write changes to state. Care should be take to prevent principals from making changes to resources as well as state. Fields: (Required) key: Path to the state file inside the S3 Bucket. Supports wildcards. (Optional) workspace_key_prefix: Prefix applied to the state path inside the bucket when using workspaces. Supports wildcards. Default: "env:" (Optional) workspaces: List of workspaces that can be accessed. Supports wildcards. Default: ["*"] (Optional) allow_default_workspace: Allow access to the default workspace (i.e. the value of key with no workspace_key_prefix). Default: true |
list(object({ |
[] |
no |
can_read | Backend configurations that can be read. This does not allow locking of state, useful when using terraform_remote_state data sources. Fields: (Required) key: Path to the state file inside the S3 Bucket. Supports wildcards. (Optional) workspace_key_prefix: Prefix applied to the state path inside the bucket when using workspaces. Supports wildcards. Default: "env:" (Optional) workspaces: List of workspaces that can be accessed. Supports wildcards. Default: ["*"] (Optional) allow_default_workspace: Allow access to the default workspace (i.e. the value of key with no workspace_key_prefix). Default: true |
list(object({ |
[] |
no |
lock_table_arn | The ARN of the DynamoDB table used for state locking. If not provided, only S3 permissions will be generated. | string |
null |
no |
lock_table_kms_key_arn | The ARN of the KMS key used to encrypt the lock table. | string |
null |
no |
name | The name for the role to be created | string |
n/a | yes |
path | The path for the role to be created | any |
null |
no |
state_bucket_arn | The ARN of the S3 bucket where the state files are stored. | string |
n/a | yes |
state_bucket_kms_key_arn | The ARN of the KMS key used to encrypt the state files. | string |
null |
no |
trust_policy | A json formatted IAM role trust policy document. | string |
n/a | yes |
Name | Description |
---|---|
role_arn | The ARN of the IAM role. |
role_name | The name of the IAM role. |