Did you know that Terraform state can – and most likely does – contain sensitive data? A few examples of sensitive information stored in the Terraform state:

When using Terraform to provision cloud infrastructure on AWS, it is common to use S3 and DynamoDB to store the Terraform state as well. When doing so, Terraform will store sensitive information on S3. By default, the confidential data is neither encrypted at rest nor protected from access from other users or roles from the same AWS account.

When using the S3 backend to manage your Terraform state, you should not forget to enable encryption-at-rest and tight access control to the S3 bucket.

Encryption-at-Rest

Enabling S3 Default Encryption will automatically encrypt the Terraform state when stored on S3. It’s only server-side encryption, but still much better than storing your sensitive information unencrypted. For full control, I recommend using a customer-managed CMK managed by the Key Management Service (KMS) when configuring the default encryption for your S3 bucket.

The following snippet shows how to enable default encryption with CloudFormation.

StateBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketEncryption:
      ServerSideEncryptionConfiguration:
      - ServerSideEncryptionByDefault:
          KMSMasterKeyID: {'Fn::ImportValue': !Sub '${ParentKmsKeyStack}-KeyArn'}
          SSEAlgorithm: 'aws:kms'
    BucketName: !Ref TerrformStateIdentifier

Access Control

First of all, use a separate S3 bucket to store your Terraform state. I recommend creating an S3 bucket per AWS account and region. Next, we need to follow the least privilege principle for read and write requests to the S3 bucket. The best way to restrict access to an S3 bucket very tightly is to make use of a bucket policy.

The following bucket policy grants the IAM role tfadmin full access to administer the S3 bucket. The IAM user tfuser is only granted read and write access to the objects within the bucket. Everyone else is neither allowed to modify the bucket nor to access the data stored within the bucket.

As the bucket policy uses Deny statements with an NotPrincipal element, it is necessary to specify the account (arn:aws:iam::111111111111:root in my example) as well as the assumed-role user when using IAM roles. Check out NotPrincipal with Deny to learn more.

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "NotPrincipal": {
        "AWS": [
          "arn:aws:iam::111111111111:root",
          "arn:aws:iam::111111111111:role/tfadmin",
          "arn:aws:sts::111111111111:assumed-role/tfadmin/session",
        ]
      },
      "NotAction": [
        "s3:ListBucket",
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::tfstate-supremeoverlord-demo",
        "arn:aws:s3:::tfstate-supremeoverlord-demo/*"
      ]
    },
    {
      "Effect": "Deny",
      "NotPrincipal": {
        "AWS": [
          "arn:aws:iam::111111111111:root"
          "arn:aws:iam::111111111111:user/tfuser",
          "arn:aws:iam::111111111111:role/tfadmin",
          "arn:aws:sts::111111111111:assumed-role/tfadmin/session",
        ]
      },
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::tfstate-supremeoverlord-demo",
        "arn:aws:s3:::tfstate-supremeoverlord-demo/*"
      ]
    }
  ]
}

Summary

Terraform will not keep your secrets! Sensitive information like database passwords, secrets stored within the Parameter Store, or shared keys for a VPN connection is at risk. Therefore, you should enable encryption-at-rest and use a bucket policy to tightly control who can access your Terraform state when using Terraform’s S3 backend.