[Terraform]機密情報を扱う際の注意点

# はじめに
Terraformは、現在もっとも人気のあるIaCツールの１つとして、インフラストラクチャの構築と管理を容易にしてくれています。一方、機密情報をいかに適切に管理するかも問題となっています。
本記事では、Terraformで機密情報を扱う際の注意点やベストプラクティスについて、AWSを中心に事例を交えてご紹介します。ぜひ参考にしていただければ幸いです。

以下のバージョンで動作確認をしています。
- Terraform v1.3.5
- hashicorp/aws v4.59.0
- hashicorp/random v3.4.3
- hashicorp/null v3.2.1

# 機密情報の記述
クレデンシャル情報を扱うリソースを作成する際に、tfファイルやtfvarsファイル上でのハードコーディングは避けましょう。代わりに、環境変数、ランダムなパスワード、外部データストア＆動的クレデンシャルなどを使うことができます。

## 環境変数を使う
例えば、「aws_db_instance」リソースでAmazon RDSインスタンスを新規作成する場合、リソースブロックの中の「username」と「password」の設定値の記述が必須（Required）です。
```terraform
resource "aws_db_instance" "example" {
  instance_class    = "db.t3.micro"
  allocated_storage = 64
  engine            = "mysql"

  # ハードコーディングはやめよう
  username          = "foo" # 必須
  password          = "foobarbaz" # 必須
}
```

`TF_VAR_変数名`という環境変数を設定することで、値を指定することができます。
```bash
$ export TF_VAR_db_username=foo
$ export TF_VAR_db_password=foobarbaz
```
```terraform
variable "db_username" {
  type = string
}
variable "db_password" {
  type = string
}

resource "aws_db_instance" "example" {
  instance_class    = "db.t3.micro"
  allocated_storage = 64
  engine            = "mysql"
  username          = var.db_username
  password          = var.db_password
}
```


一部のproviderでは直接環境変数を使う属性があります。
```bash
# クレデンシャルを環境変数に設定する
$ export AWS_ACCESS_KEY_ID=xxxxx
$ export AWS_SECRET_ACCESS_KEY=xxxxxxxx
```
```terraform
provider "aws" {
  region     = "ap-northeast-1"
  # access_keyとsecret_keyの値は環境変数から取得
}
```

providerのクレデンシャルに関しては、AWSの場合、デフォルトでは`$HOME/.aws/credentials`を参照しているため、aws cli等でクレデンシャル情報を設定済みの場合、そのまま使うことができます。
```terraform
provider "aws" {
  region     = "ap-northeast-1"
  # default以外のprofileを使う場合はprofileを指定する
  profile    = "xxxx"
}
```
また、EC2などでTerraformを実行する場合もIAMロールを使うことが一般的であるため、アクセスキーなどの記述が不要です。

## クレデンシャル生成にrandom_passwordを使う
「random」providerを使って`ランダムな文字列でクレデンシャルを生成する`ことができます。強力でランダムなパスワードをDBのパスワードに使うことが多いため、パスワード生成に「random_password」リソースがよく使われます。
※「random_string」も似たような機能を提供していますが、「random_password」の方が生成結果がコンソールに出力されないため`よりセキュア`です。
```terraform
resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

variable "db_username" {
  type = string
}

resource "aws_db_instance" "example" {
  instance_class    = "db.t3.micro"
  allocated_storage = 64
  engine            = "mysql"
  username          = var.db_username
  password          = random_password.password.result
}
```



## 外部データストア＆動的クレデンシャルを使う
AWSの場合、外部データストアとして、「Systems Manager Parameter Store」や「Secrets Manager」を使うことができます。
以下、「Secrets Manager」から「username」と「password」を取得し、DBに設定する一例です。
```terraform
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "test/db"
}

locals {
  credentials = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

resource "aws_db_instance" "example" {
  instance_class    = "db.t3.micro"
  allocated_storage = 64
  engine            = "mysql"
  username          = local.credentials["username"]
  password          = local.credentials["password"]
}
```
また、「Secrets Manager」の場合はパスワードの自動ローテーションをサポートしています。一定期間ごとにパスワードを更新できるため、動的クレデンシャルを使いたい場合は有効化します。

※Terraformで初期パスワードを設定し、後からパスワードを変更する、あるいは「Secrets Manager」による自動ローテーションを有効化する場合、lifecycleのignore_changesを追加し、「password」の変更を無視します。
```terraform
resource "aws_db_instance" "example" {
  instance_class    = "db.t3.micro"
  allocated_storage = 64
  engine            = "mysql"
  username          = var.db_username
  password          = random_password.password.result # 後から変更する

  lifecycle {
    ignore_changes = [
      # パスワードの変更を無視する
      password
    ]
  }
}
```

## その他
クレデンシャル情報を専用のファイルに記述し、そのファイルを.gitignoreに追加することでリモートリポジトリへのpushを防ぐという方法もありますが、同一ディレクトリやリポジトリにクレデンシャル情報の入ったファイルを格納するのを控えたほうが良いでしょう。

# git-secretsを導入する
git commit時に事前設定された正規表現にマッチする`機密情報が含まれているかスキャンできる`ツールです。クレデンシャル情報を誤ってリモートリポジトリにpushしてしまうのを防ぐことができるので、導入した方が良いでしょう。（詳しい使い方の説明は割愛します）

https://github.com/awslabs/git-secrets

# tfstateファイルの扱い方
Terraformで作成したリソースの情報（DBのパスワードなどの機密情報も含めて）は基本的に全部`tfstateファイルに平文で記載`されるようになっています。つまり、tfstateファイルにアクセス可能なエンティティがtfstateファイルに記載されたすべての機密情報を閲覧することができます。
そのため、backendを非公開にし、エンティティへの最小権限の付与と暗号化の実施が必要です。
※チーム開発の場合backendを使うケースがほとんどのため、ローカルPCにtfstateファイルを保管するケースは考慮しません。

## backendを非公開にする
機密情報が含まれている場合があるため、tfstateファイルを保管するbackend用ストレージは基本的に`外部公開しません`。そのため非公開設定が必要です。
Amazon S3の場合、バケット作成時に「ブロックパブリックアクセス設定」がデフォルトで有効になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2954734/6cb8bd13-7f32-9257-6143-52c88a87ee61.png)


## 最小権限の付与
tfstateファイルにアクセスする必要のあるエンティティに最小権限を付与することで、操作ミスや機密情報流出のリスクを減らせます。
例えば、Amazon S3とDynamoDBをTerraformのbackendとtfstate lockとして使う場合は、以下のIAMポリシーを作成し、ロールやユーザーにアタッチすることができます。
※s3:PutObjectとs3:GetObjectに関しては「*」ではなくプレフィックスを指定することで対象オブジェクトを絞り込むことができます。
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::<bucket name>"
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::<bucket name>/*"
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:DeleteItem",
                "dynamodb:GetItem"
            ],
            "Resource": [
                "arn:aws:dynamodb:<region>:<aws account id>:table/<table name>"
            ]
        }
    ]
}
```
また、tfstateファイルへのアクセス権限を最小権限で付与するだけでなく、リソースデプロイ時の権限も最小権限で付与したほうが良いでしょう。

## 暗号化の実施
### 転送時の暗号化
SSL/TSLなどを使ったデータ転送が該当します。（Amazon S3など）ほとんどのTerraformのbackendがSSL/TSLをサポートしています。（デフォルトで使われるためユーザー側での対策が不要な場合がほとんど）

### 保管時の暗号化
ほとんどのTerraformのbackendが保管時の暗号化をサポートしています。例えば、Amazon S3の場合、マネージドキーやKMSキーを使ったS3バケットの暗号化が該当します。
暗号化の有効化方法は各backendの公式ドキュメントをご参照ください。

# コンソール出力について
terraform planやterraform applyなどのコマンドを実行した際に、場合によっては、出力されたログに機密情報が含まれてしまうことがあります。
## local-exec
Terraformのリソースブロックの中で「provisioner」を使うことができます。中には「local-exec」のようなローカルマシンでコマンドを実行できるものがあります。
極端な例ですが、環境変数でprovider用のクレデンシャルを設定している場合、以下の処理でterraform apply時に該当情報をコンソールに出力することが可能です。
```terraform
resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "echo access_key=$AWS_ACCESS_KEY_ID; echo secret_key=$AWS_SECRET_ACCESS_KEY"
  }
}
```
クレデンシャルを環境変数に設定し、terraform applyを実行すると、クレデンシャルがコンソールに出力されてしまいます。
```bash
$ export AWS_ACCESS_KEY_ID=test
$ export AWS_SECRET_ACCESS_KEY=test
```
```
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

null_resource.example: Creating...
null_resource.example: Provisioning with 'local-exec'...
null_resource.example (local-exec): Executing: ["/bin/sh" "-c" "echo access_key=$AWS_ACCESS_KEY_ID; echo secret_key=$AWS_SECRET_ACCESS_KEY"]
null_resource.example (local-exec): access_key=test
null_resource.example (local-exec): secret_key=test
null_resource.example: Creation complete after 0s [id=xxxxxxxxxxxxxxxxxxx]
```

それだけでなく、ローカルマシンのファイルシステムにもアクセスできるため、以下の処理も実行可能です。
```terraform
resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "cat ~/.aws/credentials"
  }
}
```
```
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

null_resource.example: Creating...
null_resource.example: Provisioning with 'local-exec'...
null_resource.example (local-exec): Executing: ["/bin/sh" "-c" "cat ~/.aws/credentials"]
null_resource.example (local-exec): [default]
null_resource.example (local-exec): aws_access_key_id = xxxxx
null_resource.example (local-exec): aws_secret_access_key = xxxxxxxx
null_resource.example: Creation complete after 0s [id=xxxxxxxxxxxxxxxxxxx]
```

「local-exec」などのprovisionerを使うと、一部の処理をTerraformでコントロールできなくなるので、`セキュリティリスクが高まります`。それを使うのを避けたほうが良いでしょう。

## 変数の値をコンソール出力から隠す
tfの変数にクレデンシャル情報を直接記述することは非推奨ですが、クレデンシャル以外の値でもコンソール出力からは隠しておきたい場合があります。この場合、変数ブロックに`sensitive = true`を記述することで、コンソール出力から該当変数の値を隠すことができます。
例えば、aws_vpcを作成する際に、vpc_cidrという変数のsensitive属性をtrueに設定することで、terraform plan時の出力を隠すことができます。
```terraform
variable "vpc_cidr" {
  type      = string
  default   = "10.1.0.0/16"
  sensitive = true
}

resource "aws_vpc" "example" {
  cidr_block = var.vpc_cidr
}

```

sensitiveがfalseの場合は、cidr_blockの値がterraform plan時に出力されます。
```
  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.1.0.0/16"
      ...
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.
```
sensitiveをtrueにすると、cidr_blockの値が出力されなくなります。
```
  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                                  = (known after apply)
      + cidr_block                           = (sensitive value)
      ...
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.
```


# まとめ
- 機密情報を記述する際に、ハードコーディングではなく、環境変数、ランダムなパスワード、外部データストア＆動的クレデンシャルなどを使う
- git-secretsを導入し、クレデンシャル情報を誤ってリモートリポジトリにpushするのを防ぐ
- tfstateファイルを扱う際に、backendを非公開にし、エンティティへの最小権限の付与とファイルの暗号化を実施する
- セキュリティリスクを減らすために「local-exec」などのprovisionerの使用を避ける
- コンソールに出力したくない値がある場合sensitive属性を使う

# 参考
- [hashicorp/aws](https://registry.terraform.io/providers/hashicorp/aws/4.59.0)
- [hashicorp/random](https://registry.terraform.io/providers/hashicorp/random/latest/docs)
- [Backend Configuration](https://developer.hashicorp.com/terraform/language/v1.3.x/settings/backends/configuration)
- [Provisioners](https://developer.hashicorp.com/terraform/language/v1.3.x/resources/provisioners/syntax)
- [Protect Sensitive Input Variables](https://developer.hashicorp.com/terraform/tutorials/configuration-language/sensitive-variables)