Published on

GCP Secret Manager에서 비밀값을 OpenTofu로 관리하기

Authors
  • avatar
    Name
    Jay
    Twitter

IaS(Infrastructure As Code)로 GCP Secret Manager의 Secret 값을 관리할 때, 어떻게 하면 좋을까? Terraform을 이용하여 tf파일을 Git으로 관리하는 것을 생각해볼 수 있는데, Private Git Repository를 사용한다고 하더라도 tf파일에 Secret 값들을 평서문으로 올리는 것은 바람직하지 않다.

kms_secret_asymmetric

이럴 때, Terraform google_kms_secret_asymmetric data source를 사용하는 것을 고려해볼 수 있다.

GCP의 KMS(Key Management Service)를 이용하여 비대칭키를 생성하여 관리하고, Terraform Resource Definition에는 공개키로 암호화된 값을 작성한다. 최종적으로 Secret Manager의 Secret 값은 Runtime중에 google_kms_secret_asymmetric data source를 통해서 복호화된 값이 설정되게 된다.

Hashcorp google provider 문서를 보면 어떻게 사용할 수 있는지 자세히 설명되어 있다. google_kms_secret_asymmetric data source는 terraform-provider-google-beta provider에서 제공되기 때문에 google-beta를 설정해준다.

This resource is in beta, and should be used with the terraform-provider-google-beta provider. See Provider Versions for more details on beta resources.

main.tf

terraform {
  required_providers {
    google-beta = {
      source = "hashicorp/google-beta"
      version = "6.14.1"
    }
  }
}

provider "google" {
  project = "tutorial"
  region  = "us-central1"
}

provider "google-beta" {
  project = "tutorial"
  region  = "us-central1"
}

이제 아래처럼 GCP KMS에서 비대칭키를 사용하기 위하여 resource를 정의하고, 최종적으로 local환경에서 사용할 public key를 저장한다.

main.tf

data "google_kms_key_ring" "tutorial_keyring" {
  name     = "tutorial-keyring"
  location = "us-central1"
}

resource "google_kms_crypto_key" "tutorial_secret" {
  name     = "tutorial_secret"
  key_ring = data.google_kms_key_ring.tutorial_keyring.id
  purpose  = "ASYMMETRIC_DECRYPT"
  version_template {
    algorithm = "RSA_DECRYPT_OAEP_4096_SHA256"
  }
}

data "google_kms_crypto_key_version" "tutorial_secret" {
  crypto_key = google_kms_crypto_key.tutorial_secret.id
}

resource "local_file" "public_key" {
    filename = "${path.module}/public-key.pem"
    content     = data.google_kms_crypto_key_version.tutorial_secret.public_key[0].pem
}

이제 생성된 public key로 원하는 값을 암호화한다.

echo -n hello  |
  openssl pkeyutl -in - \
        -encrypt \
        -pubin \
        -inkey public-key.pem \
        -pkeyopt rsa_padding_mode:oaep \
        -pkeyopt rsa_oaep_md:sha256 \
        -pkeyopt rsa_mgf1_md:sha256 \
        -out tutorial.enc

그리고 crc32 checksum을 저장한다.

$HOME/go/bin/crc32 -polynomial castagnoli < tutorial.enc

마지막으로 암호화했던 것을 base64로 인코딩 한다.

openssl base64 \
  -in tutorial.enc \
  -out tutorial.enc.b64

이렇게 계산한 base64 인코딩 값을 ciphertext에 저장하고, checksum을 위해서 crc32 값도 추가해준다.

data "google_kms_secret_asymmetric" "secret_data" {
  crypto_key_version = data.google_kms_crypto_key_version.opentofu-secret.id
  crc32              = "920961a5"
  ciphertext         = <<EOT
    UCIDSu19B3c3twcdppvPUGfu+QWtXEJdCgEru80jwW7r0Zzq1nNIEW9ZaFYGlUUT
    xreYLOF+VsIY5ejnzr0QKuU7Y/0vnZrc7RU6WBNW8R/IyrOXT8LB6e1KwfomduaI
    4VMrx2WQxo+kaU1Tw2p6JoAKZICzoaZZg2eatMESxlxLlMIQTK6QDbtD0mOu1rzI
    rz257/jI1FNzxLq9K3BmfxjFWCuL45QnyM0Y8Aqh0YXyVCiPQNr/GodR8JNKl/xt
    rHAEq8YL/r1lOGCJNBsFYjcKIAUD5Ade1hVfUQGZDMNM/wcCLCOU3aHhgkaAT6xG
    3FjZtyIYW2dnpIelHrtSxepQN4yy49qxeuKaf9Cjj6uXJsTi0bCiTw4PcD4RHOTJ
    Gbm2QLJMr5BaiUyK0ZXHokynQg++McAiRsygeuWilInXPLpEkDcC+G0+i6Z2f3Ki
    6kOE9edC5YHFSH4mrUeFZwdbdPgPzfP1szJ+h6jMJJt76jSDovAkwScuh2eibvRG
    wO+O0VZaZW9WdiwX7yD4GQSkCRTZg4XQJ6TSuG+U18T33mOoQbA0DvvYcYYRMVBS
    q3YVhbGhBwxfxFXRoWYBqyGomP33NL6cEKWnovwPf3OSPDZ8n66nqzQY18tJvr+7
    XALVYFRpEdOCBjBa1EbB+e0LKeL8LyREq/3aGX7+X5Q=
  EOT
  provider = google-beta
}

resource "google_secret_manager_secret" "secret_data" {
  secret_id = "secret_key_name"

  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "secret_data" {
  secret = google_secret_manager_secret.secret_data.id

  secret_data = data.google_kms_secret_asymmetric.secret_data.plaintext
}

이렇게 Secret Manager의 Secret 값을 저장하면, Terraform Resource 정의에서 평서문으로 작성하지 않고 runtime과정에서 kms을 통해서 복호화해서 저장하게 된다. Git에는 평서문 대신에 공개키로 암호화된 값을 저장하면 된다. 하지만 공식문서에 나온 것처럼 Secret값이 log, plan, state 출력값에 노출되는 것에 대해서는 주의를 해야 한다.

Using this data provider will allow you to conceal secret data within your resource definitions, but it does not take care of protecting that data in the logging output, plan output, or state output. Please take care to secure your secret data outside of resource definitions.

Terraform으로 apply하고 생성된 state 파일을 보면, 아래와 같이 plaintext값이 "hello"로 노출되어 있는 것을 확인할 수 있다.

"attributes": {
    "ciphertext": "생략",
    "crc32": "생략",
    "crypto_key_version": "생략",
    "id": "생략",
    "plaintext": "hello"
}

Terraform에서는 remote state로 S3와 같은 Object Storage에 저장하도록 설정이 가능하고, S3의 경우에는 물리적인 공간에 저장될 때 암호화가 되어서 저장된다. 하지만 S3에 저장되는 state 파일 object는 동일하게 비밀값이 평서문으로 노출되어 있다. 따라서 해당 비밀값이 노출되지 않도록 S3 bucket에 접근을 잘 관리해야 한다.

OpenTofu State 암호화

OpenTofu를 사용할 때는 state 파일 자체를 암호화하는 옵션을 이용할 수 있다. 아래와 같이 GCP Key Management Service를 통해서 State 파일을 암호화하여 저장할 수 있다. Cloud Key Managerment Service를 이용할 때는 키를 잃어버릴 가능성이 거의 없고, 실수로 삭제하더라도 일정기간 보존하기 때문에 복구할 수 있는 옵션이 존재한다. 따라서 State 파일을 KMS로 암호화하여 보관할 때, 키를 잃어버려서 다시 복호화를 못하는 문제에 대해서 덜 걱정해도 된다고 생각된다.

terraform {
  encryption {
    key_provider "gcp_kms" "basic" {
      kms_encryption_key = "KMS key 경로 값"
      key_length = 32
    }
    method "aes_gcm" "secure_method" {
      keys = key_provider.gcp_kms.basic
    }
    method "unencrypted" "migrate" {}
    state {
      method = method.aes_gcm.secure_method
      fallback {
        method = method.unencrypted.migrate
      }
    }
  }
  required_providers {
    google-beta = {
      source = "hashicorp/google-beta"
      version = "6.14.1"
    }
  }
}

결론

GCP Secret Manager의 비밀값을 Terraform으로 관리할 때, google_kms_secret_asymmetric data source를 사용하여 공개키로 암호화된 값으로 코드 관리를 할 수 있다. 하지만 State, Output 같은 경로로 복호화된 평서문이 노출될 수 있기 때문에 이점에 대해서 주의를 해야 한다. Terraform의 remote state를 사용할 경우에는 state 파일에는 평서문으로 저장되기 때문에 해당 파일이 노출되지 않도록 주의해야 한다. OpenTofu를 이용할 경우에는 State 파일 자체를 암호화해서 저장할 수 있는 옵션이 있다. Cloud Key Management Service를 이용하는 경우에는 키를 잃어버려서 다시 복호화하지 못하는 문제가 발생할 가능성이 상당히 적기 때문에, OpenTofu를 통해서 State 파일 자체를 암호화하여 저장하는 것도 고려해볼 수 있겠다.