Skip to content

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.

Notifications You must be signed in to change notification settings

george-richardson/terraform-aws-state-access-role

Repository files navigation

Terraform S3 Backend State Access Role Module

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.

Usage

At its simplest, this module can be used to generate a role with minimal permissions for a given backend configuration.

Example ConfigurationTerraform 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"
        }
    ]
}

Working with encrypted state

You can pass in state_bucket_kms_key_arn and lock_table_kms_key_arn to grant least privilege permissions for underlying KMS keys.

Permissions Policy Submodule

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
}

Requirements

Name Version
terraform >= 1.2.0
aws >= 4.9.0

Providers

Name Version
aws >= 4.9.0

Modules

Name Source Version
permissions_policy ./modules/permissions-policy n/a

Resources

Name Type
aws_iam_role.this resource
aws_iam_role_policy.inline_policy resource
aws_iam_policy_document.trust data source

Inputs

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({
key = string
workspace_key_prefix = optional(string, "env:")
workspaces = optional(list(string), ["*"])
allow_default_workspace = optional(bool, true)
}))
[] 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({
key = string
workspace_key_prefix = optional(string, "env:")
workspaces = optional(list(string), ["*"])
allow_default_workspace = optional(bool, true)
}))
[] 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({
key = string
workspace_key_prefix = optional(string, "env:")
workspaces = optional(list(string), ["*"])
allow_default_workspace = optional(bool, true)
}))
[] 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

Outputs

Name Description
role_arn The ARN of the IAM role.
role_name The name of the IAM role.

About

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.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages