明明标了 sensitive,状态文件里却留着明文密码
한국어 원문으로 표시합니다.
목표
도구가 만든 비밀번호가 상태·JSON 출력·저장한 계획의 어디에 평문으로 남는지 직접 찾고, OpenTofu 의 상태·계획 암호화를 기존 평문 상태에서 이주해 강제한 뒤, 비밀을 상태 밖에 두는 구성을 만듭니다.
왜 중요한가
sensitive 는 사람이 보는 화면에서 값을 가리는 표시일 뿐, 도구가 저장하는 기록에는 값이 그대로 들어갑니다. 상태 파일과 CI 가 올리는 계획 파일이 곧 비밀 저장소가 되는 셈이라, 이것을 모르면 '가렸으니 안전하다' 고 착각한 채 상태를 아무 곳에나 둡니다. 막는 방법은 세 층입니다: 저장 시 암호화(키가 곧 상태의 열쇠가 됨), 평문으로 되돌아가지 못하게 강제, 그리고 애초에 값을 도구에 넘기지 않고 참조만 넘기는 설계입니다.
단계
/root/iac-secret/main.tf에random_password.db(길이 20, 특수문자 없음), 그 값을out/db.conf에password=<값>으로 쓰는local_sensitive_file.dbconf, 그리고sensitive = true인 출력db_password를 두세요. init·apply 한 뒤tofu output에 값이 가려져 보이는지 확인합니다.- 상태 파일에서 비밀번호 값이 들어 있는 모든 자리를 찾아
/root/iac-secret/leak.txt에 JSON 경로를 한 줄에 하나씩 적으세요. 경로는 키와 배열 번호를 점으로 이은 형식입니다(예:resources.0.instances.0.attributes.x). 채점기는tofu state pull로 받은 상태에서 같은 계산을 해 비교합니다. tofu output의 출력을/root/iac-secret/masked.txt에 저장하세요. 그다음tofu plan -out=/root/iac-secret/leak.tfplan으로 계획을 저장하고, 계획 파일(zip) 안의tfstate항목에 비밀번호가 몇 번 나오는지 세어/root/iac-secret/planleak.txt에planleak=<수>로 적으세요./root/iac-secret/encryption.tf에 민감한 변수state_passphrase와 상태·계획 암호화를 선언하세요: pbkdf2 키 제공자(패스프레이즈는 그 변수), aes_gcm 방법, 그리고 기존 평문을 읽기 위한 unencrypted 방법을 state·plan 의 fallback 으로 둡니다. 패스프레이즈는labhub-state-passphrase-01이고 환경 변수TF_VAR_state_passphrase로 넘깁니다. apply 하면 terraform.tfstate 가 암호화돼야 합니다(파일에 비밀번호가 없고 encrypted_data 가 있음).encryption.tf에서 unencrypted 방법과 두 fallback 을 지우고 state·plan 에enforced = true를 두세요. 올바른 패스프레이즈로는 계획이 깨끗해야 합니다. 틀린 패스프레이즈(TF_VAR_state_passphrase=wrong-passphrase-000000)로tofu plan을 돌린 출력을/root/iac-secret/wrongkey.txt에 저장하세요. 채점기는 사본에서 틀린 키로 실제로 읽기가 실패하는지 확인합니다.- 올바른 패스프레이즈로
tofu plan -out=/root/iac-secret/safe.tfplan을 저장하세요. 이 파일에는 비밀번호가 평문으로 없어야 하고 zip 도 아니어야 합니다. 키가 있으면tofu show -json safe.tfplan이 되고, 키가 없으면 실패하는지 확인하세요. /root/iac-secret/ref/secret/db.key에openssl rand -hex 16으로 비밀을 만들고 권한을 600 으로 두세요(도구 밖에서 만든 비밀)./root/iac-secret/ref/main.tf에는 random_password 없이out/db.conf에password_file=<그 경로>한 줄을 쓰는local_file.dbconf만 두고 init·apply 합니다. 채점기는 ref 의 상태 어디에도 db.key 의 값이 없고 경로만 있는지 봅니다.
참고
- 파드에는 OpenTofu 1.9.0 과 local·random 프로바이더 미러가 있어 인터넷 없이 돕니다. 4단계부터는 셸마다
export TF_VAR_state_passphrase='labhub-state-passphrase-01'를 먼저 실행하세요. - 암호화를 켠 뒤에는
tofu state pull이 복호화된 상태를 보여 줍니다. 파일을 직접 열면 encrypted_data 만 보입니다. - 값을 상태·계획에 아예 남기지 않는 ephemeral 리소스와 write-only 속성은 OpenTofu 1.11 에서 들어왔습니다. 이 파드의 1.9.0 에는 없어서 참조 방식으로 대신합니다.
- 흔한 실수: 4단계에서 fallback 없이 암호화를 켜 기존 평문 상태를 못 읽는 것. 흔한 실수: 패스프레이즈를 main.tf 기본값에 적어 저장소에 커밋하는 것.
- Sensitive Data in State · State and Plan Encryption · Ephemerality · Terraform: Sensitive data
비밀번호를 만들고 출력은 가린다
/root/iac-secret/main.tf 에 random_password.db(길이 20, 특수문자 없음), 그 값을 out/db.conf 에 password=<값> 으로 쓰는 local_sensitive_file.dbconf, 그리고 sensitive = true 인 출력 db_password 를 두세요. init·apply 한 뒤 tofu output 에 값이 가려져 보이는지 확인합니다.
random_password 의 result 는 프로바이더가 민감한 값으로 표시합니다. 그 값을 출력으로 내보내려면 출력에도 sensitive 를 붙여야 계획이 통과합니다. local_sensitive_file 은 내용을 화면에 보이지 않게 할 뿐 파일은 똑같이 씁니다.
가린 비밀이 상태 파일의 어디에 있는가
상태 파일에서 비밀번호 값이 들어 있는 모든 자리를 찾아 /root/iac-secret/leak.txt 에 JSON 경로를 한 줄에 하나씩 적으세요. 경로는 키와 배열 번호를 점으로 이은 형식입니다(예: resources.0.instances.0.attributes.x). 채점기는 tofu state pull 로 받은 상태에서 같은 계산을 해 비교합니다.
tofu output -raw db_password 로 값을 얻을 수 있습니다(가림은 표시 방식일 뿐이라는 증거이기도 합니다). jq 의 paths(scalars) 와 getpath 로 값이 같은 자리를 모두 찾을 수 있습니다. 한 자리만 있지 않습니다.
가림은 화면에서만 — JSON 출력과 저장한 계획
tofu output 의 출력을 /root/iac-secret/masked.txt 에 저장하세요. 그다음 tofu plan -out=/root/iac-secret/leak.tfplan 으로 계획을 저장하고, 계획 파일(zip) 안의 tfstate 항목에 비밀번호가 몇 번 나오는지 세어 /root/iac-secret/planleak.txt 에 planleak=<수> 로 적으세요.
저장한 계획 파일은 zip 이고, 그 안에 계획을 세울 때의 상태 사본이 들어 있습니다. unzip -l 로 항목을 보고 unzip -p <파일> tfstate 로 꺼내 셉니다. CI 가 계획 파일을 산출물로 올리면 그 산출물도 비밀이 됩니다.
평문 상태를 암호화 상태로 옮긴다
/root/iac-secret/encryption.tf 에 민감한 변수 state_passphrase 와 상태·계획 암호화를 선언하세요: pbkdf2 키 제공자(패스프레이즈는 그 변수), aes_gcm 방법, 그리고 기존 평문을 읽기 위한 unencrypted 방법을 state·plan 의 fallback 으로 둡니다. 패스프레이즈는 labhub-state-passphrase-01 이고 환경 변수 TF_VAR_state_passphrase 로 넘깁니다. apply 하면 terraform.tfstate 가 암호화돼야 합니다(파일에 비밀번호가 없고 encrypted_data 가 있음).
암호화를 켜기만 하면 도구가 기존 평문 상태를 읽기를 거부합니다(조작된 평문일 수 있어서). 그래서 한 번은 평문을 읽을 수 있게 fallback 을 두고 적용해 새 방법으로 다시 쓰게 합니다. 패스프레이즈는 16자 이상이어야 합니다.
이주가 끝나면 평문 대체를 빼고 강제한다
encryption.tf 에서 unencrypted 방법과 두 fallback 을 지우고 state·plan 에 enforced = true 를 두세요. 올바른 패스프레이즈로는 계획이 깨끗해야 합니다. 틀린 패스프레이즈(TF_VAR_state_passphrase=wrong-passphrase-000000)로 tofu plan 을 돌린 출력을 /root/iac-secret/wrongkey.txt 에 저장하세요. 채점기는 사본에서 틀린 키로 실제로 읽기가 실패하는지 확인합니다.
fallback 을 남겨 두면 누군가 평문 상태를 끼워 넣어도 읽힙니다. enforced 는 평문으로 쓰는 것 자체를 막습니다. 키를 잃으면 상태를 영영 못 읽으니 키 보관이 곧 상태 보관입니다.
암호화된 계획 파일은 키 없이는 읽을 수 없다
올바른 패스프레이즈로 tofu plan -out=/root/iac-secret/safe.tfplan 을 저장하세요. 이 파일에는 비밀번호가 평문으로 없어야 하고 zip 도 아니어야 합니다. 키가 있으면 tofu show -json safe.tfplan 이 되고, 키가 없으면 실패하는지 확인하세요.
3단계의 leak.tfplan 과 나란히 head -c 2 로 앞 두 바이트를 비교해 보세요. zip 은 PK 로 시작합니다. 계획 암호화는 state 와 별도로 plan 블록이 정합니다.
비밀은 상태 밖에 두고, 상태에는 참조만 넣는다
/root/iac-secret/ref/secret/db.key 에 openssl rand -hex 16 으로 비밀을 만들고 권한을 600 으로 두세요(도구 밖에서 만든 비밀). /root/iac-secret/ref/main.tf 에는 random_password 없이 out/db.conf 에 password_file=<그 경로> 한 줄을 쓰는 local_file.dbconf 만 두고 init·apply 합니다. 채점기는 ref 의 상태 어디에도 db.key 의 값이 없고 경로만 있는지 봅니다.
도구가 값을 만들거나 읽으면 그 값은 상태로 들어갑니다. 값을 쓰는 쪽(애플리케이션)이 파일이나 비밀 저장소에서 직접 읽게 하고, 도구는 위치만 알려 주는 구성이 가장 단순한 회피입니다. 데이터 소스로 비밀 파일을 읽어도 상태에 남는다는 점에 주의하세요.