한 줄 요약: 평문 Secret 11개(307키)를 Vault KV로 옮기고 ESO로 재생성하는 컷오버를 전부 마쳤습니다. 해시 일치 11/11, 컷오버로 인한 파드 재시작 0건. 다만 실제로 시간을 쓴 곳은 이행이 아니라, 안전장치라고 믿었던 것들이 제 역할을 못 하고 있다는 걸 하나씩 발견한 쪽이었습니다.
이 글의 위치: 기술 블로그 → 인프라 · 로컬 서버 운영. 작성·검증 방식은 Editorial Policy 참조. 집 PC 한 대로 굴리는 홈랩 검증기 시리즈입니다.
이 글에서 한 일 — 한눈에
평문 Secret 11개를 Vault로 옮기는 작업이었는데, 정작 기록으로 남은 것은 안전장치들이 제 역할을 못 하고 있던 쪽이었습니다.
secrets관심사가 완전히 비어 있었습니다 — Vault 를 첫 입주자로 놓고 잘못됐을 때의 파급 범위를 기준으로 스킬 셋으로 쪼갰습니다.- 비밀이 새는 곳은 코드가 아니라 실행 방식이었습니다 — argv 와 디스크 두 경로를 막았습니다.
- "검사하지 못했다"를 "실패했다"로 쓰고 있었습니다 — 같은 모양의 오판을 네 건 잡았습니다.
- 사전 검사를 전부에 걸면 롤백이 막힙니다 — 정확히 필요한 순간에 못 쓰는 안전장치가 됩니다.
- 404 는 차단이 아니었습니다 — 게이트가 아예 안 걸려 있었고, 판정 기준이 틀렸습니다.
- 멱등이 영원히 수렴하지 않았습니다 — Vault CLI 인자 순서 때문에 실패가 빈 값으로 흘렀습니다.
- 카나리 첫 시도는 실패했고 자동 복원이 정확히 동작했습니다 — 원인은 이름 매칭이었습니다.
- 11개 서비스를 전부 컷오버했습니다 — 307키 해시 일치 11/11, 재시작 0건.
- Postgres 재시작은 검증 가치가 없었습니다 — 아무것도 증명 못 하면서 순단만 만드는 검증이었습니다.
- 자원 걱정은 기우였습니다 — Vault 5파드 합계 CPU 7m · RAM 155Mi.
아래부터 순서대로 풀어씁니다.
왜 스킬부터 만들고 클러스터는 나중에 건드렸나
비밀 관리를 손보기로 하고 가장 먼저 연 것은 클러스터가 아니라 제 플러그인 저장소였습니다. 거기에 secrets 라는 관심사(concern) 자리가 있었는데, 안이 완전히 비어 있었습니다.
provides:
patterns: []
skills: []
commands: []
Vault 를 그 자리의 첫 입주자로 놓기로 했습니다. 그리고 하나의 큰 스킬 대신 blast radius, 즉 잘못됐을 때 무엇까지 망가지는가를 기준으로 셋으로 쪼갰습니다.
| 스킬 | 맡는 일 | 잘못되면 |
|---|---|---|
vault-init |
설치·초기화·봉인해제·스냅샷 | Vault 자체가 못 뜸 |
vault-sync |
정책·인증·소비 경로를 선언대로 수렴 | 구성이 어긋남 |
vault-adopt |
기존 평문 Secret 을 비파괴로 인수 | 운영 중인 서비스가 멈춤 |
가장 위험한 것만 따로 떼어 두면, 나머지를 반복해서 돌리는 동안 그 하나만 조심하면 됩니다.
비밀이 새는 곳은 코드가 아니라 실행 방식이었습니다
Vault 의 unseal 키는 그 자체가 최상위 비밀입니다. 코드에 하드코딩하지 않는 건 당연한데, 그것만으로는 부족했습니다. 실행하는 방식이 키를 흘리고 있었습니다.
argv 와 디스크
토큰을 이렇게 넘기면 노드의 프로세스 목록에 그대로 남습니다. 같은 노드에 들어올 수 있는 누구나 ps 한 번으로 봅니다.
kubectl exec vault-0 -- env VAULT_TOKEN=hvs.xxxxx vault ... # argv 노출
operator init 의 출력을 파일로 받는 것도 같습니다. 나중에 암호화하더라도 평문이 디스크에 닿은 순간이 존재합니다. 그래서 두 경로를 다 막았습니다 — 모든 Vault 호출은 토큰을 stdin 첫 줄로만 받고, operator init 의 출력은 파일을 거치지 않고 파이프로 곧장 암호화기로 흘려보냅니다.
교훈: “비밀을 코드에 안 넣었다”는 검사는 너무 이릅니다. argv · 디스크 · 로그 세 곳을 따로 봐야 합니다.
“검사하지 못했다”를 “실패했다”로 쓰고 있었습니다
이 계열의 버그를 이번에 네 번 잡았습니다. 전부 같은 모양입니다 — 확인할 수 없는 상태를 확인해 보니 나쁜 상태로 보고합니다.
- 초기화도 하기 전인데
--verify가 “root 토큰 인증 실패”라고 했습니다. 봉인 파일이 존재하지도 않는 시점이었습니다. --plan이 “passphrase 를 설정하세요”라며 아직 만들지도 않은 비밀번호를 찾게 만들었습니다.- 공개 주소가 게이트에 막혔는지를 상태 코드로 판정했습니다. 뒤에 나오지만 이게 정반대 결론을 냈습니다.
- passphrase 를 입력받을 수 있는지를
[[ -e /dev/tty ]]로 판정했습니다. 에이전트 셸에서 그 장치는 존재하지만 열리지 않습니다 — 사용자는 프롬프트를 못 본 채 “인증 실패”만 받습니다.
고치는 방향은 하나였습니다. 바깥 원인부터 순서대로 짚게 만드는 것입니다. 파드가 떠 있는가 → 초기화됐는가 → 봉인 파일이 있는가 → 봉인 상태인가 → passphrase 가 있는가. 그리고 실제로 열어보는 검사로 바꿉니다.
다만 전부에 적용하면 안 됩니다
이 사전 검사를 모든 동작에 걸면 롤백이 봉인 때문에 막힙니다. 되돌려야 하는 순간은 대개 뭔가 잘못된 순간이고, 그때 Vault 가 봉인돼 있을 수 있습니다. 정확히 필요한 순간에 쓸 수 없는 안전장치가 됩니다. 그래서 Vault 를 직접 만지는 동작에만 걸고, 조회·롤백·렌더는 봉인 중에도 돌게 두었습니다.
404 는 차단이 아니었습니다
Vault 를 vault.forgemanager.store 로 노출하면서 Cloudflare Access 로 게이팅했습니다. 확인해 보니 404 가 떨어졌습니다. “막혀 있구나” 하고 넘어갈 뻔했습니다.
404 는 도달하지 못했다는 뜻이 아니었습니다. 오히려 요청이 터널까지 다 도달했고 뒤에서 경로를 못 찾은 것입니다. 원인은 IP 정책이 allow 가 아니라 bypass 로 만들어져 있었기 때문이었습니다. bypass 는 그 IP 에서 오면 인증을 통째로 건너뜁니다. 제가 테스트한 곳이 바로 그 IP 였습니다.
교훈: 게이트가 걸렸는지는 상태 코드가 아니라 어디로 보내는지로 판정해야 합니다. 응답의
location:이 로그인 화면을 가리키는지 보는 게 정확합니다.
문서에는 “Allow · Include IP” 라고 적혀 있었는데 코드는 bypass 를 만들고 있었습니다. 문서와 동작이 어긋난 채로 다른 서비스들은 우연히 allow 로 만들어져 정상이었고, 하필 비밀 저장소에서 처음 드러났습니다.
멱등이 영원히 수렴하지 않은 이유
적용은 전부 성공했는데, 곧바로 다시 확인하면 정책 생성 필요 가 남아 있었습니다. 몇 번을 돌려도 같았습니다.
원인은 Vault CLI 의 인자 순서였습니다. 플래그를 위치 인자 뒤에 두면 이렇게 됩니다.
vault policy read foo -format=json # → Too many arguments (항상 실패)
vault read secret/foo -format=json # → 플래그가 URL 쿼리로 삼켜짐 (?-format=json)
둘 다 조용히 빈 값을 돌려주고, 코드는 그걸 “객체가 없다”로 읽습니다. 그래서 매번 생성 대상이 되고, 영원히 수렴하지 않습니다. 또 실패를 없음으로 오해한 것입니다 — 앞 절과 같은 계열입니다.
플래그를 서브커맨드 뒤로 옮기고, 사용법 오류는 빈 값으로 흘리지 않고 즉시 중단하도록 고쳤습니다. 적용 동작이 스스로를 재확인하지 않았다면 조용히 넘어갔을 자리입니다.
컷오버 — 카나리 하나에서 열한 개로
순서는 “백업 → 원본 삭제 → ESO 가 새로 생성” 으로 고정했습니다. ESO 가 기존 Secret 을 인수할지가 버전마다 다르기 때문에, 어느 버전에서도 같은 결과가 나오는 순서를 택한 것입니다.
첫 카나리는 실패했습니다. 그리고 자동 복원이 정확히 동작해 원본 25키가 그대로 돌아왔습니다. 원인은 조회 키였습니다 — 넘긴 인자는 k8s Secret 이름(script-service-env)인데 찾고 있던 것은 리소스 이름(script-service)이었습니다. 둘은 애초에 같을 이유가 없습니다.
고치고 재시도한 뒤로는 나머지 열 개도 순차로 넘어갔습니다.
| 항목 | 결과 |
|---|---|
| 컷오버한 서비스 | 11 / 11 |
| 키 총합 | 307 키 (서비스당 3~78) |
| 해시 일치 | 11 / 11 |
| 워크로드 상태 | 17 개 전부 1/1 |
| 컷오버로 인한 재시작 | 0 건 |
평문 Secret 은 클러스터에서 사라졌고 값의 단일 출처는 Vault KV 가 됐습니다.
Postgres 재시작은 검증 가치가 없었습니다
자격증명이 제대로 넘어갔는지 확인하려고 Postgres 를 재시작하려다 멈췄습니다. Postgres 엔트리포인트는 POSTGRES_USER/PASSWORD 를 초기화할 때만 읽습니다. 데이터 디렉터리에 이미 PG_VERSION 이 있으면 재시작 시엔 아예 보지 않습니다.
즉 자격증명이 틀려도 드러나지 않고 맞아도 증명되지 않습니다. 그런데 그 DB 에 7개 서비스가 의존합니다. 아무것도 증명하지 못하면서 순단만 만드는 검증입니다.
대신 그 자격증명을 런타임에 실제로 쓰는 exporter 두 개를 무중단으로 재시작해 DB 인증이 실제로 되는지 확인했습니다. 증명하고 싶은 것을 실제로 쓰는 지점을 골라야 합니다.
정리 — 안전장치가 안전하지 않을 때
이번 작업에서 반복된 것은 기술 스택이 아니라 한 가지 실패 모양이었습니다.
- 검사하지 못한 것을 실패로 칠했습니다 — 초기화 전 인증 오류, 없는 passphrase 요구
- 실패를 없음으로 읽었습니다 — CLI 사용법 오류가 빈 값으로 흘러 멱등이 깨졌습니다
- 신호 대신 대용물로 판정했습니다 — 404 를 차단으로,
/dev/tty존재를 입력 가능으로 - 안전장치가 정작 필요할 때 막았습니다 — 봉인 때문에 롤백이 안 되는 구조
공통점은 전부 판정 로직이라는 것입니다. 옮기는 코드가 아니라 “됐는지 확인하는” 코드에서 나왔습니다. 이행 자체는 오히려 순조로웠습니다 — 해시 일치 11/11 에 재시작 0 건.
덧붙여, 자원 걱정은 기우였습니다. Vault 계열 5 파드 합계가 CPU 7m · RAM 155Mi 로, 4 vCPU · 31Gi 노드에서 CPU 0.18% · RAM 0.5% 였습니다. 같은 노드의 CNI 하나가 그 백 배를 씁니다.
답글 남기기