目の前に僕らの道がある

勉強会とか、技術的にはまったことのメモ

Terraformのsopsプロバイダーを使用するだけで機密情報は守られるのか

qiita.com

この記事は、3-shake Advent Calendar 2023の9日目の記事となります。

sops プロバイダーとは

sopsプロバイダーはMozilla sopsを使用して暗号化されたファイルから機密情報を取り出して、terraform上で使用できるようにしたものです。 暗号化の鍵をAWS KMS等を使うことにより、KMSキーを使う権限を持つ人だけ機密情報にアクセスできるようにするものです。 sopsで機密情報を暗号化することにより、平文で機密情報をgitレポジトリに保存することがなくなり安全ということになります。

機密情報を管理したい。でも平文では保存したくない。そういう用途にこちらは使用されます。

本当に安心?

SOPSを使って機密情報を暗号化することによりgitレポジトリには機密情報が平文で残らない。

これで安心と言われていますが、よく考えると機密情報をterraform実行時にはリソースに対して平文で与えているはずです。 つまり、tfstate上は機密情報が平文で保存されています。

例えば、tfstateがS3に保存されているとして、KMSキーへの権限がない人でもS3バケットにアクセスする権限があれば、平文の機密情報が見れてしまいます。 あまりないと思いますが、tfstateをlocalに保存するようにしていてそれをgit管理していてらなんのために暗号化しているのか。。。。ということになります。

こう考えると組織のポリシーによるが、sopsプロバイダーによる暗号化では不十分ではないかという疑問が生まれます。

ドキュメントを調べる

まずプロバイダードキュメントを当たってみます。

Docs overview | carlpett/sops | Terraform | Terraform Registry

To prevent plaintext secrets from being written to disk, you must use a secure remote state backend. See the official docs on Sensitive Data in State for more information.

これが意味してるのはバックエンドをlocalにした場合平文で機密情報が書かれるので、安全なリモートバックエンドを利用すべきということだと思います。

State: Sensitive Data | Terraform | HashiCorp Developer

参照しろと言われたドキュメントの該当部分を読んでみましょう。

ローカルディスクにtfstateを保存した場合は、機密情報が平文で保存されます。リモートにtfstateを保存する場合、保存時に暗号化されるかはバックエンドに依存します。

基本的にリモートステートを使うことを推奨しています。 例えば、Terraform Cloudを使う場合、tfstateは暗号化され、転送時もTLSで暗号化されます。 S3を使う場合もSSE-S3やSSE-KMS等でサーバサイド暗号化を有効にしておくことで、保管時の暗号化がされます。バケットポリシーでHTTPSを強制することで通信時の暗号化も保証することができます。

参考: 暗号化によるデータの保護 - Amazon Simple Storage Service

参考: Amazon S3 のセキュリティのベストプラクティス - Amazon Simple Storage Service

ところがですね。保存時、通信時の暗号化をしても、terraform state pullすると平文でtfstateが手に入ってしまうんですよ。。。後述します。

挙動を実験する

以下のような設定ファイルを作ります。sopsで暗号化したdb_userとdb_passwordをパラメータストアに設定するものになります。

tools-versions

terraform 1.5.5
sops 3.7.3

main.tf

terraform {
  required_version = "~> 1.5.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.15"
    }
    sops = {
      source  = "carlpett/sops"
      version = "~> 0.7.2"
    }
  }
  backend "s3" {
    region  = "ap-northeast-1"
    bucket  = "xxxxxxxxxx"
    key     = "test.tfstate"
  }
}

provider "sops" {
}

provider "aws" {
  region = "ap-northeast-1"
}

data "sops_file" "secrets" {
  source_file = "secrets.yaml"
}

resource "aws_ssm_parameter" "db_user" {
  type     = "String"
  name     = "/test/db_user"
  value    = data.sops_file.secrets.data.db_user
}

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    = data.sops_file.secrets.data.db_password
}

暗号化前の secrets.yaml

db_user: user
db_password: password

apply結果がこちらとなります。

terraform apply

% export SOPS_KMS_ARN=arn:aws:kms:ap-northeast-1:xxxxxxxxx:key/yyyyyyyyyyyyyyyyyy
% terraform apply
data.sops_file.secrets: Reading...
data.sops_file.secrets: Read complete after 1s [id=-]

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ssm_parameter.db_password will be created
  + resource "aws_ssm_parameter" "db_password" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_password"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "SecureString"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

  # aws_ssm_parameter.db_user will be created
  + resource "aws_ssm_parameter" "db_user" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_user"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

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

aws_ssm_parameter.db_password: Creating...
aws_ssm_parameter.db_user: Creating...
aws_ssm_parameter.db_user: Creation complete after 0s [id=/test/db_user]
aws_ssm_parameter.db_password: Creation complete after 0s [id=/test/db_password]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
terraform apply  8.91s user 0.78s system 124% cpu 7.811 total

state showするとパラメータストアなのでsensitive扱いになっていて、見れません。これはいけるか?

terraform state show

% terraform state show aws_ssm_parameter.db_password
# aws_ssm_parameter.db_password:
resource "aws_ssm_parameter" "db_password" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password"
    data_type = "text"
    id        = "/test/db_password"
    key_id    = "alias/aws/ssm"
    name      = "/test/db_password"
    tags_all  = {}
    tier      = "Standard"
    type      = "SecureString"
    value     = (sensitive value)
    version   = 1
}

% terraform state show aws_ssm_parameter.db_user    
# aws_ssm_parameter.db_user:
resource "aws_ssm_parameter" "db_user" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_user"
    data_type = "text"
    id        = "/test/db_user"
    name      = "/test/db_user"
    tags_all  = {}
    tier      = "Standard"
    type      = "String"
    value     = (sensitive value)
    version   = 1
}

ここで、terraform state pullをしてみて、tfstateファイルをローカルにダウンロードします。

そのtfstateファイルの中の該当部分はこちらとなります。

    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "db_password",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password",
            "data_type": "text",
            "description": "",
            "id": "/test/db_password",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "/test/db_password",
            "overwrite": null,
            "tags": null,
            "tags_all": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "password",
            "version": 1
          },
          "sensitive_attributes": [
            [
              {
                "type": "get_attr",
                "value": "value"
              }
            ]
          ],
          "private": "bnVsbA==",
          "dependencies": [
            "data.sops_file.secrets"
          ]
        }
      ]
    },

tfstateファイルの中身をよく確認するとしっかり平文で見えています。残念。

"value": "password",

結論

sopsプロバイダーを使用することによりgitレポジトリ上に機密情報を平文で保存することはなくなります。 しかしながら、tfstateのデータ上では設定値が平文で保存されることを防ぐことはできません。terraform state pullする権限があれば、機密情報が見れてしまいます。

運用組織のポリシーで、tfstateへのアクセス権限を適切に権限管理することができるのであれば、選択肢としては取りうります。 暗号化のためのKMSキー、tfstateを保存するS3バケットを機密情報をアクセス可能な人のみ権限を与えることが徹底できればよいです。 しかしながら、機密情報をいかなる場合でもローカルに平文で保存することが許容されない組織であれば、機密情報は手動で設定することを選択したほうが望ましいと思います。

どうしても機密情報をterraformで管理したのであれば、クライアントサイドで暗号化した機密情報をterraformで管理し、アプリ等で使用時にクライアントサイドで復号を行う形も考えられます。

安全かどうかは、tfstateの保存場所、tfstateへのアクセス権限、暗号化鍵のアクセス権限それぞれが適切に設定されているかどうかが鍵となります。

他に何かうまい方法で機密情報を管理しているという方がいらっしゃれば、ご意見ください。

ワークアラウンド

これは自分がよく使う手段となります。

リソースの箱だけ作って、作成時にダミーの値を入れておき、実際の値は手動で設定するという手法です。 ignore_changesを入れておくことで、手動で値を変更しても、terraform的には差分ができないようにしています。 これにより、機密情報をterraformの外に追い出しつつも、機密情報を入れるリソース自体は監理するということが実現できます。

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    =  "Dummy"
  lifecycle {
    ignore_changes = [value]
  }
}