IT/IaC

OpenTofu state encryption 내부, 우리가 PBKDF2 대신 KMS로 간 이유

gfrog 2026. 5. 29. 12:49
반응형

state 파일은 IaC를 운영하는 사람한테 늘 골치 아픈 물건이다. RDS 마스터 패스워드, IAM access key, 가끔은 슬랙 토큰까지, 다 평문으로 박혀있다. S3에 SSE 걸어두면 됐다고 생각했었는데, 작년에 인턴이 실수로 state 파일을 깃에 푸시한 일이 있고 나서 그 마음이 좀 달라졌다. S3 암호화는 결국 S3 안에서만 의미가 있다. 다운로드 받는 순간 끝이다.

OpenTofu 1.7에서 state encryption이 GA로 풀린 게 2024년 5월이다. 우리 팀은 그때는 "재밌네" 정도로만 보고 넘겼는데, 1.10 즈음에서 진지하게 검토를 시작했고 결국 올해 초에 도입했다. 그러다 1.11.4의 보안 픽스 노트를 보고 한 번 더 들여다보게 됐다. 이번 글은 도입 과정에서 내가 결국 이해해야 했던 내부 동작, 그리고 PBKDF2 대신 KMS로 넘어간 이유를 정리한 노트다.

state encryption은 단일 암호화가 아니다

처음에 문서를 봤을 때 헷갈렸던 게, key_provider 와 method 가 따로 있다는 점이었다. 그냥 "키 줘, 암호화해줘" 가 아니라 두 단계로 쪼개져 있다. 사실 내부적으로는 KEK/DEK 패턴이다.

key_provider 가 KEK (Key Encryption Key) 를 만들고, method 는 그 KEK 로 실제 DEK (Data Encryption Key) 를 다루는 식이다. PBKDF2 key_provider 는 비밀번호 문자열을 받아서 PBKDF2 로 KEK 를 유도한다. AWS KMS key_provider 는 KMS 의 GenerateDataKey API 를 호출해서 평문 DEK 와 암호화된 DEK 를 한 번에 받아온다. method 는 AES-GCM 이 표준이고, state 파일 안에 nonce 와 ciphertext 가 같이 저장된다.

처음에 이 구조가 왜 이렇게 됐는지 잘 몰랐는데, fallback 메커니즘을 보고 나서야 납득이 됐다.

terraform {
  encryption {
    key_provider "aws_kms" "primary" {
      kms_key_id = "arn:aws:kms:ap-northeast-2:111122223333:key/abcd-1234"
      region     = "ap-northeast-2"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "primary" {
      keys = key_provider.aws_kms.primary
    }

    state {
      method = method.aes_gcm.primary
      fallback {
        # 암호화 전 state도 일단 읽을 수 있어야 한다
      }
    }
  }
}

fallback 블록을 비워두면 "예전 평문 state 도 읽을 수 있다" 가 된다. 마이그레이션 한 번 돌리고 나면 fallback 을 빼면 된다. 이 두 단계 구조 덕분에 키 로테이션할 때도 옛 키를 fallback 으로 두고 새 키로 점진적으로 갈아탈 수 있다. 운영하면서 이게 진짜 중요했다.

PBKDF2 를 처음에 골랐다가 빼버린 이유

처음 PoC 때는 PBKDF2 로 시작했다. 키 인프라가 따로 필요 없고, 그냥 환경변수에 passphrase 만 던지면 되니까 빠르게 검증할 수 있었다. 근데 막상 팀에 배포하려고 보니 문제가 줄줄이 나왔다.

첫째, passphrase 를 어디에 둘 거냐. 결국 1Password 든 Vault 든 어딘가 시크릿 스토어가 필요한데, 그러면 처음부터 KMS 가 낫다. KMS 는 IAM 으로 권한 통제가 되고 CloudTrail 에 access 로그가 남는다. PBKDF2 passphrase 는 누가 언제 꺼내봤는지 추적이 안 된다.

둘째, PBKDF2 iteration count 가 의외로 까다롭다. OpenTofu 가 기본값을 계속 올리고 있는데 (최근에 600,000 까지 올라온 걸로 안다), 너무 낮게 잡힌 옛날 state 를 마이그레이션할 때 뜨는 경고 메시지를 보고 좀 불안해졌다. 그리고 최근에 PBKDF2 관련 글들이 도는 걸 보면, "iteration 수 충분치 않으면 brute force 가능하다" 라는 지적이 계속 나오고 있다. 무한정 올릴 수 있는 것도 아니고 (init/plan 마다 KEK 유도 비용을 다 치러야 한다), 답이 깔끔하지 않다.

셋째, 사람마다 같은 passphrase 를 공유한다는 게 보안 모델로서 깔끔하지 않다. 누가 퇴사하면 키를 돌려야 하는데 그게 또 한 번의 작업이다. KMS 면 IAM 권한만 회수하면 된다.

그래서 PoC 한 달쯤 굴려보고 KMS 로 넘어갔다. 비용 영향은 크지 않았다. plan/apply 마다 GenerateDataKey 1번, Decrypt 1번 정도라 월 KMS 비용이 무시할 만한 수준이었다.

1.11.4 의 zip 보안 픽스, 우리 환경에 영향이 있었나

5월 초에 1.11.4 가 나오면서 init 단계 zip 처리 관련 보안 픽스가 같이 들어왔다. 처음에 release note 를 보고 "어 우리 영향 있는 건가" 하고 멘탈이 살짝 흔들렸는데, 결론부터 말하면 우리는 영향 없었다. provider 다운로드 단계에서 만들어진 zip 을 OpenTofu 가 풀 때 path traversal 이 가능한 케이스를 막은 픽스였고, 우리는 사내 mirror 를 쓰고 있어서 noise 없는 환경이었다.

다만 JSON state encryption syntax 가 바뀐 것은 영향이 있었다. 이전에는 key_provider 의 keys 필드를 JSON 으로 표현할 때 template interpolation 이 안 먹는 경우가 있었는데 1.11.4 에서 풀렸다. 우리는 Atlantis 에서 동적으로 워크스페이스마다 다른 KMS 키를 쓰고 있어서, 이 부분 덕분에 코드가 좀 깔끔해졌다. 그동안 우회로 처리하던 게 정식 문법으로 풀린 셈이다.

1.12.0 도 얼마 전에 나왔고, 거기서는 input variable 을 apply 단계에 다시 줄 수 있게 되면서 state encryption 설정에 변수를 쓰는 패턴이 좀 더 자연스러워졌다. 우리는 아직 1.11.x 에 머무르고 있는데 (1.11 시리즈는 2026-08-01 까지 지원), 1.12 의 dynamic prevent_destroy 가 매력적이라 6월쯤 옮길 생각이다.

plan 파일도 암호화 대상이라는 점

도입 초기에 놓쳤다가 사고 직전까지 갔던 것 하나가, plan 파일이다. terraform plan -out=tfplan 으로 떨군 plan 파일에도 변수와 state diff 가 그대로 들어있다. 이게 평문이면 state 만 암호화한 의미가 반쪽이다.

OpenTofu encryption 블록에서 state {} 와 plan {} 을 각각 따로 켜야 한다. 우리는 CI 파이프라인에서 plan artifact 를 S3 에 잠깐 올렸다가 apply 단계에서 다시 받아 쓰는 구조라, plan 암호화가 의외로 중요했다. 처음에는 state 만 켜놓고 며칠 운영하다가 코드 리뷰에서 지적받고 부랴부랴 추가했다.

state {
  method = method.aes_gcm.primary
}

plan {
  method = method.aes_gcm.primary
}

별로 어렵지 않다. 그런데 빼먹기 쉽다. 도입할 거면 처음부터 둘 다 켜고 가는 걸 추천한다.

그래서 도입할 만한가

솔직히 말하면, S3 SSE + bucket policy 만 잘 잠가두면 일상 운영에서는 비슷한 효과가 난다. state encryption 의 진짜 가치는 state 가 S3 밖으로 나갔을 때, 그리고 CI 머신 디스크 같은 중간 경유지에 평문이 안 남는다는 점에서 나온다. 우리 팀은 인턴 사고 이후로 그게 정량화할 수 없는 가치라고 결론을 냈다.

다만 처음 도입할 때 KEK/DEK 두 단계 구조, fallback 메커니즘, plan 파일까지 같이 켜야 한다는 점을 미리 알고 시작해야 덜 헤맨다. 그리고 PBKDF2 는 빠른 PoC 에만 쓰고, 본운영은 KMS (혹은 GCP KMS, Vault transit) 로 가는 게 정답에 가깝다는 게 1년 가까이 굴려본 결론이다.

아직 풀지 못한 숙제도 있다. KMS 키 자체의 로테이션 (AWS 의 automatic key rotation 을 켜면 DEK 가 다른 key material 로 다시 감싸지는데, OpenTofu 입장에서 어떻게 보이는지 검증이 더 필요하다), 그리고 멀티 리전 운영 시 키 정책 통일 같은 게 남아있다. 다음에는 그쪽도 정리해보려고 한다.

반응형