Ansible Vault로 Git 저장소 내 비밀 정보 암호화하기
Ansible Vault를 사용하여 git에 저장된 vars 파일과 문자열을 안전하게 암호화하는 방법을 설명합니다. ansible-vault encrypt와 encrypt_string의 차이점, 그리고 운영 환경별 비밀 관리 전략을 확인하십시오.
Ansible Vault가 보호하는 것과 보호하지 않는 것
Ansible Vault는 플레이북 저장소 내부의 비밀 정보를 암호화하므로, git에는 일반 텍스트 암호 대신 암호문이 저장됩니다. ansible-vault 명령은 사용자가 선택한 암호에서 파생된 대칭 키를 사용하여 파일 전체 또는 파일 내의 단일 값을 암호화합니다. 플레이가 실행될 때 Ansible은 메모리에서 해당 내용을 복호화하므로, 변수는 다른 일반 변수와 동일하게 동작합니다.
이 모델에는 한 가지 명확한 경계가 있습니다. Vault는 저장소에 정지 상태(at rest)로 있는 비밀 정보만을 보호할 뿐 그 이상은 보호하지 않습니다. 작업이 실행되면 해당 값은 메모리, 렌더링된 템플릿, 모듈 인자, 그리고 실행 결과 출력물에서 일반 텍스트 상태가 됩니다(별도로 차단하지 않는 한). 플레이북을 실행할 수 있는 모든 사람은 Vault 암호를 알고 있으므로, Vault는 팀 외부 사람으로부터의 비밀 유지는 보장하지만 팀 내부의 개인별 접근 제어는 제공하지 않습니다.
아직 플레이북을 작성하지 않았다면 VPS를 대상으로 하는 첫 번째 Ansible 플레이북부터 시작하고, 해당 플레이북에 암호가 필요해지면 다시 돌아오십시오.
파일 전체를 암호화할 것인가, 단일 문자열을 암호화할 것인가?
ansible-vault encrypt은 파일을 암호문으로 대체합니다. 파일은 $ANSIBLE_VAULT로 시작하는 헤더 아래에 base64 텍스트 블록 하나로 변환됩니다. 파일에 비밀 정보만 포함되어 있을 때 사용하십시오.
ansible-vault encrypt_string는 하나의 값을 암호화하여 일반 vars 파일에 붙여넣을 수 있는 YAML 스니펫을 출력합니다. 변수 이름은 읽을 수 있는 상태로 유지되며 값만 암호문이 됩니다. 비밀 정보가 일반 텍스트 설정과 섞여 있을 때 사용하십시오.
실무에서 중요한 차이점은 diff 결과입니다. Vault 파일은 저장할 때마다 새로운 무작위 salt로 다시 암호화되므로 암호문의 모든 바이트가 변경됩니다. 따라서 git diff을 실행하면 읽을 수 없는 블록이 다른 읽을 수 없는 블록으로 대체된 것으로 표시되며, 검토자는 비밀번호를 교체한 것인지 파일을 다시 작성한 것인지 알 수 없습니다. 반면 encrypt_string을 사용하면 각 비밀 정보가 일반 텍스트 파일 내의 개별 블록으로 존재하므로, diff를 통해 어떤 변수가 변경되었는지 정확히 파악할 수 있고 나머지 파일 내용은 그대로 유지됩니다.
인라인 방식에는 비용이 따르며, 이는 교체 시점에 발생합니다. ansible-vault rekey는 인라인 블록을 수정하지 않습니다. 비밀 정보 목록이 길고 변경이 거의 없는 경우에는 파일 방식을 선택하십시오. 비밀 정보와 일반 변수가 섞여 있고 코드 리뷰의 의미를 살리고 싶다면 인라인 방식을 선택하십시오.
보호 대상이 명시된 group_vars 레이아웃
Ansible은 group_vars/<group>.yml을 로드하며, group_vars/<group>/ 디렉터리 내부의 모든 파일도 함께 로드합니다. 디렉터리 방식을 사용하는 것이 좋습니다. 이 방식을 사용하면 하나의 그룹에 일반 텍스트 파일과 암호화된 파일을 나란히 배치할 수 있기 때문입니다.
inventory/
hosts.ini
group_vars/
all/
vars.yml
vault.yml
web/
vars.yml
vault.yml
host_vars/
db01/
vars.yml
vault.yml
playbooks/
site.yml모든 vault.yml는 암호화되어 있습니다. 모든 vars.yml은 일반 텍스트입니다. 파일 이름만으로도 어떤 값이 보호되는지 알 수 있으므로, 파일을 열어보지 않아도 보호 대상을 파악할 수 있습니다.
이 패턴의 두 번째 핵심은 간접 참조입니다. 암호화된 파일 내부의 모든 변수 이름 앞에 vault_ 접두사를 붙입니다.
vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"그런 다음 옆에 있는 일반 텍스트 파일에서 해당 이름을 참조합니다.
db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"역할(Role)과 템플릿은 db_password을 사용하며 값이 어디에서 왔는지 알 필요가 없습니다. 이를 통해 플레이북과 역할 간의 분리가 깔끔하게 유지됩니다. 일반 텍스트인 vars.yml는 검색 가능한 인덱스 역할도 겸합니다. 즉, grep -r vault_ group_vars/은 복호화 과정 없이도 저장소에서 요구하는 모든 비밀 정보를 나열합니다. 이 방식의 비용은 비밀 정보 하나당 추가적인 이름이 하나 더 필요하다는 점이며, vault_ 이름에 오타가 있을 경우 구문 오류가 아닌 실행 시점에 정의되지 않은 변수 오류로 나타납니다.
encrypt_string으로 변수 하나 암호화하기
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
--stdin-name 'vault_db_password'비밀 값을 입력한 뒤 Ctrl-D를 누릅니다. --stdin-name는 표준 입력에서 값을 읽어오므로 셸 기록 파일에 값이 남지 않습니다. 다른 방식처럼 명령줄에 값을 직접 입력하면 셸 기록에 남게 됩니다.
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
'a real password' --name 'vault_db_password'어떤 방식을 사용하든 명령은 YAML 블록을 출력합니다. !vault 태그 아래의 들여쓰기는 값의 일부이므로, 출력된 그대로 vars 파일에 붙여넣어야 합니다.
vault_db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
6638643965323633646262656665306333616466396630323136393465356136396436383331
3131303163306665326539353837343663313762616561306534373963383531613664393332!vault 태그는 YAML 로더에게 해당 스칼라 값이 일반 텍스트가 아닌 암호문임을 알립니다. 헤더에는 형식 버전, 암호화 알고리즘, 그리고 암호화에 사용된 vault ID 레이블이 포함됩니다. vault ID 없이 암호화된 값은 레이블이 없는 1.1 헤더를 가지며, 이 경우에도 정상적으로 작동하지만 해당 비밀 값이 어디에서 유래했는지에 대한 정보는 적게 제공합니다.
Vault 비밀번호는 어디에 저장됩니까?
저장소 외부입니다. 이는 예외가 없는 유일한 규칙입니다.
--ask-vault-pass은 실행 시마다 한 번씩 입력을 요청하며 아무것도 저장하지 않습니다. 노트북 환경에는 적합하지만, cron 작업이나 CI 러너에는 적합하지 않습니다.
비밀번호 파일은 첫 번째 줄에 비밀번호가 포함된 일반 텍스트 파일입니다. 권한을 엄격하게 설정하여 빈 파일을 생성한 뒤 편집기로 내용을 채우면, 비밀번호가 셸 히스토리에 남지 않습니다.
mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt--vault-password-file을 사용하여 모든 명령에서 해당 파일을 지정하십시오.
ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
--vault-password-file ~/.ansible/vault-prod.txt모든 명령에 해당 플래그를 반복해서 입력하는 것은 잊기 쉽습니다. 따라서 저장소 루트의 ansible.cfg에 한 번만 설정하십시오.
[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt동일한 설정이 환경 변수 ANSIBLE_VAULT_PASSWORD_FILE에서도 읽히며, CI 작업은 일반적으로 이 방식을 통해 비밀번호를 제공합니다. 작업은 자체 자격 증명 저장소에서 비밀번호를 가져와 임시 디렉터리의 파일에 기록하고, 변수를 export한 뒤 작업이 끝나면 파일을 삭제합니다. ansible.cfg의 경로는 커밋되므로, 누군가 체크아웃 내부에 실제 파일을 생성할 가능성이 있습니다. 따라서 .gitignore에도 파일 이름 패턴을 추가하십시오.
비밀번호 파일이 실행 가능한 경우, Ansible은 파일을 텍스트로 읽는 대신 파일을 실행하고 표준 출력에서 비밀번호를 읽어옵니다. 이것이 디스크에 기록하지 않고 시스템 키링이나 클라우드 비밀 관리자에서 vault 비밀번호를 가져오는 방법입니다. --vault-id를 통해 사용하는 스크립트에는 추가 요구 사항이 있습니다. 이름이 -client으로 끝나거나 -client와 확장자로 끝나야 하며, 실행 가능해야 하고, --vault-id 옵션을 허용해야 하며, 비밀번호를 표준 출력으로 출력해야 합니다.
두 개의 vault ID: 스테이징 및 프로덕션
vault ID는 vault 비밀번호에 첨부되는 레이블이며 label@source와 같이 작성합니다. 소스는 prompt, 비밀번호 파일 경로 또는 클라이언트 스크립트 경로입니다. 레이블을 사용하면 하나의 저장소에서 여러 비밀번호로 비밀을 관리할 수 있으므로, 스테이징 비밀번호로 프로덕션 파일을 열 수 없습니다.
ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
group_vars/prod/vault.yml실행 시 필요한 모든 ID를 전달하십시오:
ansible-playbook playbooks/site.yml \
--vault-id staging@~/.ansible/vault-staging.txt \
--vault-id prod@~/.ansible/vault-prod.txt또는 ansible.cfg에 한 번 나열하십시오:
[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt한 가지 동작이 사용자들을 놀라게 할 수 있습니다. 기본적으로 레이블은 잠금 장치가 아니라 힌트입니다. Ansible은 파일이 복호화될 때까지 현재 보유한 모든 비밀을 파일에 대입해 보므로, staging로 레이블이 지정된 파일이라도 프로덕션 비밀번호가 올바른 키라면 열립니다. [defaults] 아래에 vault_id_match = True을 설정하거나 환경 변수 ANSIBLE_VAULT_ID_MATCH를 설정하면, Ansible은 레이블이 파일 헤더와 일치하는 비밀만 사용합니다. 이 확인 작업은 1.2 헤더를 필요로 하므로, 애초에 vault ID로 암호화된 콘텐츠에만 적용됩니다.
여러 ID가 로드된 경우, ansible-vault encrypt는 어떤 비밀번호로 암호화해야 할지 알 수 없습니다. --encrypt-vault-id prod로 이름을 지정하거나 ansible.cfg에 vault_encrypt_identity을 설정하여 저장소에 기본값을 지정하십시오.
이 방식의 이점은 배포 범위의 분리입니다. 스테이징을 배포하는 CI 작업에는 스테이징 비밀번호만 제공되므로, 침해된 러너(runner)는 프로덕션 자격 증명을 읽을 수 없습니다. 하나의 제어 머신에서 여러 Linux 서버를 관리하는 환경에서 이러한 분리는 작은 사고와 매우 큰 사고를 가르는 차이가 됩니다.
퇴사자가 발생했을 때 볼트(vault) 키 재설정하기
키 재설정(rekey)은 볼트 비밀번호를 변경하고 새로운 비밀번호로 내용을 다시 암호화하는 과정입니다. 이 작업이 이전의 기록을 되돌리지는 않습니다. 기존 비밀번호를 알고 있던 사람은 누구나 자신이 보관 중인 저장소 사본을 복호화할 수 있으며, 해당 사본에 포함된 모든 과거 커밋도 마찬가지입니다. 따라서 비밀번호 소지자가 떠나는 즉시 해당 비밀번호는 폐기된 것으로 간주하고, 다음 순서에 따라 교체해야 합니다.
- 서버와 타사 서비스의 실제 자격 증명을 변경합니다. 이 단계가 실질적으로 접근 권한을 회수하는 과정입니다.
ansible-vault edit을 사용하여 새로운 값을 볼트 파일에 입력합니다.- 암호화된 모든 파일을 새로운 볼트 비밀번호로 재설정합니다.
- 저장소가 아닌 별도의 채널을 통해 새로운 볼트 비밀번호를 필요한 사람들에게 전달합니다.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
--new-vault-id prod@prompt \
group_vars/prod/vault.yml host_vars/db01/vault.ymlrekey는 한 번의 명령으로 여러 파일을 처리할 수 있으며, --new-vault-id prod@prompt은 디스크에서 읽는 대신 새로운 비밀번호를 한 번만 입력받습니다. 레이블을 변경해야 할 특별한 이유가 없다면 기존 레이블을 그대로 유지하십시오. 레이블은 명령이 다시 작성하는 모든 파일의 헤더에 기록되기 때문입니다.
인라인 형식을 사용할 때 발생하는 비용이 바로 이 지점입니다. ansible-vault rekey은 완전히 암호화된 파일을 대상으로 작동하므로, 일반 텍스트 vars 파일 내부에 있는 !vault 블록은 변경되지 않은 상태로 남습니다. 먼저 해당 블록들을 찾아낸 다음, 새로운 비밀번호를 사용하여 encrypt_string으로 각각 다시 생성해야 합니다.
grep -rl '!vault' group_vars/ host_vars/이것이 전체적인 트레이드오프입니다. 인라인 블록을 사용하면 읽기 쉬운 diff를 얻을 수 있지만, 교체 시점에 수동 작업이 필요합니다. 완전히 암호화된 파일은 하나의 명령으로 교체할 수 있지만, 검토 시 유용한 정보를 제공하지 않습니다.
출력물에 비밀 정보가 계속 나타나는 이유
Vault는 값이 복호화되는 순간 그 역할을 마칩니다. Ansible은 작업 결과를 보고하는데, 인자를 그대로 출력하는 모듈은 해당 자격 증명을 보고서에 포함하게 됩니다. 상세 모드(verbose) 실행, 템플릿 작업에서의 --diff 사용, 인자를 덤프하는 작업 실패, 또는 출력물을 파일로 기록하는 콜백 플러그인 등은 모두 평문 정보를 남길 수 있습니다. 파일을 암호화하는 것만으로는 이러한 경로를 막을 수 없습니다.
no_log: true가 해결책입니다. 자격 증명을 다루는 모든 작업에 이 설정을 적용하십시오.
- name: Write the application environment file
ansible.builtin.template:
src: app.env.j2
dest: /etc/myapp/app.env
owner: myapp
group: myapp
mode: "0600"
no_log: true그러면 Ansible은 해당 작업의 결과를 출력에서 제외하므로, 로그에는 작업이 수행되었다는 사실만 남고 무엇을 처리했는지는 기록되지 않습니다. 특히 루프(loop)를 사용할 때는 반드시 설정하십시오. 루프는 항목마다 결과를 보고하며, 자격 증명 목록을 순회할 경우 전체 목록이 노출되기 때문입니다.
no_log로는 방지할 수 없는, 복호화된 비밀 정보가 유출되는 네 가지 경로가 더 있습니다.
- 템플릿으로 생성된 파일은 지정한
mode및owner을 상속받습니다. 자격 증명을 포함하는 모든 파일에는mode: "0600"와 특정 소유자를 설정하십시오. 그렇지 않으면 대상 호스트에서 누구나 해당 파일을 읽을 수 있게 됩니다. ansible.builtin.command또는ansible.builtin.shell에 전달된 비밀 정보는 명령이 실행되는 동안 대상 호스트의 프로세스 목록에 나타나며, 로컬 사용자가 이를 읽을 수 있습니다. 대신 파일이나 환경 변수를 통해 전달하십시오.- 팩트 캐싱(Fact caching)은 수집된 팩트를 제어 노드의 디스크에 기록합니다. 따라서 비밀 정보를 담고 있는 등록 변수가 민감한 정보라고 생각하지 못한 캐시 파일에 저장될 수 있습니다.
- 동일한 비밀 정보는 보통 컨테이너가 읽는 환경 파일과 같이 두 번째 장소에도 존재합니다. 해당 환경의 규칙은 별개이므로, Compose 환경 파일에서 자격 증명 제외하기에서 관련 내용을 확인하십시오.
no_log를 사용하면 디버깅이 어려워지는데, 이는 의도된 동작입니다. 작업이 정상적으로 작동하지 않을 때 테스트 호스트에서 일시적으로 제거하고, 변경 사항을 운영 환경에 적용하기 전에 다시 설정하십시오.
평문 파일을 남기지 않고 암호화된 파일 읽고 편집하기
ansible-vault view group_vars/prod/vault.yml은(는) 복호화 내용을 페이저(pager)로 출력하며 디스크에 아무것도 기록하지 않습니다. ansible-vault edit은(는) 임시 파일로 복호화하여 $EDITOR을(를) 열고, 편집기를 닫으면 다시 암호화합니다. 작업 트리에 평문 파일을 남기는 ansible-vault decrypt보다 두 도구를 사용하는 것을 권장합니다. 실수로 스테이징된 복호화된 볼트(vault) 파일은 실제 자격 증명이 공개 저장소로 유출되는 가장 흔한 경로입니다.
Git은 암호화된 파일을 즉석에서 복호화하여 읽을 수 있는 diff를 생성할 수 있습니다.
git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes이 설정을 활성화하기 전에 해당 명령이 수행하는 작업을 이해해야 합니다. 이제 git diff은(는) 운영 환경의 비밀 값을 터미널에 출력하며, 이는 스크롤백 기록이나 화면 공유 시 노출될 수 있습니다. 이는 한 대의 머신에서 한 명의 사용자가 사용하는 로컬 편의 기능이므로, git config은(는) 로컬에 유지하십시오. 다른 사용자가 동일한 설정을 하지 않는 한, 다른 사람의 체크아웃 환경에서는 다르게 동작할 것임을 인지해야 합니다.
Vault가 적절한 도구가 아닐 때
Vault는 레이블당 하나의 비밀번호를 저장하는 파일 형식이며, 이러한 구조적 한계로 인해 사용 범위가 제한됩니다. 다음 중 하나라도 해당한다면 본격적인 비밀 관리 도구(secret store)로 전환해야 합니다.
- 사용자별 접근 제어가 필요할 때: 플레이북을 실행하는 모든 사람이 동일한 비밀번호를 공유합니다. Vault ID는 환경별로 접근을 분리할 뿐, 사용자별로 분리하지 않습니다.
- 감사 추적(audit trail)이 필요할 때: Vault는 누가 무엇을 언제 복호화했는지 기록하지 않습니다.
- 주기적인 교체(rotation)가 필요할 때: Vault는 만료나 버전 관리 기능을 제공하지 않으므로, 자격 증명이 2년 동안 변경되지 않았더라도 이를 알 수 있는 방법이 없습니다.
- 애플리케이션이 런타임에 직접 비밀을 읽어야 할 때: 부팅 시 데이터베이스 비밀번호를 읽어오는 서비스가 배포 저장소에 있는 파일을 읽어서는 안 됩니다.
이 시점부터는 패턴이 반대로 바뀝니다. Ansible은 비밀을 직접 저장하지 않고, 런타임에 lookup 플러그인을 통해 HashiCorp Vault(이름은 비슷하지만 다른 제품), 클라우드 제공업체의 Secret Manager, 또는 제어 노드의 키링에서 비밀을 가져옵니다. 저장소에는 경로만 남고, 실제 값과 접근 로그는 관리 도구에서 처리합니다. 소규모 팀이라면 API를 지원하는 자체 호스팅 비밀번호 관리자인 Vaultwarden 서버를 사용하여 동일한 작업을 더 가볍게 수행할 수 있습니다.
단, 하나의 자격 증명은 이 모든 체계에서 제외됩니다. 제어 노드가 서버에 접속하기 위해 사용하는 SSH 키는 Vault의 관리 대상이 아닙니다. Ansible이 플레이를 실행하기 전에 이미 이 키가 필요하기 때문입니다. 이는 SSH 키 관리 기초에서 다루는 방식대로 에이전트와 암호(passphrase)를 사용하여 관리하십시오.
FAQ
vars 파일 전체를 암호화해야 합니까, 아니면 비밀 문자열만 암호화해야 합니까?
파일에 비밀 정보만 포함되어 있다면 전체를 암호화하십시오. 하나의 명령어로 전체를 교체할 수 있고 구조가 단순하게 유지되기 때문입니다. 비밀 정보가 일반 변수와 섞여 있다면 ansible-vault encrypt_string를 사용하십시오. 이 경우 암호화된 값만 diff에 나타나므로 검토자가 어떤 변수가 변경되었는지 확인할 수 있습니다. 대신 교체 작업이 번거로워집니다. ansible-vault rekey은 파일 전체를 대상으로 하며 인라인 !vault 블록은 건드리지 않으므로, 새 암호를 사용할 경우 해당 블록은 수동으로 다시 생성해야 합니다.
Ansible Vault 암호 파일은 어디에 저장해야 합니까?
저장소 외부의 ~/.ansible/vault-prod.txt과 같은 경로에 저장하고 권한을 0600로 설정하십시오. --vault-password-file로 해당 파일을 지정하거나, ansible.cfg의 [defaults] 아래에 vault_password_file를 설정하거나, 환경 변수에 ANSIBLE_VAULT_PASSWORD_FILE을 설정하십시오. CI 환경에서는 작업이 자체 자격 증명 저장소에서 암호를 가져와 임시 파일에 기록하고, 환경 변수로 내보낸 뒤 작업이 끝나면 파일을 삭제하도록 구성하십시오. 파일이 실행 가능하면 Ansible은 이를 실행하여 표준 출력에서 암호를 읽어오므로, 디스크에 저장하는 대신 키링에서 암호를 가져올 수 있습니다.
스테이징과 운영 환경에 서로 다른 vault 암호를 사용하려면 어떻게 해야 합니까?
--vault-id staging@/path/to/file와 --vault-id prod@/path/to/file을 사용하여 각 암호에 레이블을 지정하고, 각 환경의 파일을 해당 레이블로 암호화하십시오. 실행 시 두 ID를 모두 전달하거나 [defaults] 아래의 vault_identity_list에 나열하십시오. 기본적으로 Ansible은 파일이 복호화될 때까지 보유한 모든 비밀 정보를 시도하므로, 파일 헤더와 일치하는 레이블의 비밀 정보만 시도하게 하려면 vault_id_match = True을 설정하십시오. 여러 ID가 로드된 상태에서 암호화에 사용할 ID를 선택하려면 --encrypt-vault-id를 사용하십시오.
Ansible Vault는 실행 출력에 암호가 나타나는 것을 방지합니까?
아니요. Vault는 저장소에 저장된 상태의 비밀 정보만 보호합니다. 작업이 실행되면 값은 평문이 되며, 상세 모드(verbose)로 실행하거나 작업이 실패할 경우 로그에 노출될 수 있습니다. 자격 증명을 다루는 모든 작업에 no_log: true를 추가하고, 템플릿으로 생성하는 모든 파일에 제한적인 mode 및 owner 권한을 설정하십시오. 또한 비밀 정보를 명령 인수로 전달하지 마십시오. 명령이 실행되는 동안 대상 호스트의 프로세스 목록에 노출되기 때문입니다.