OpenBao 자동 언실

OpenBao(dev/prod 클러스터의 시크릿 저장소)가 재시작할 때 사람 개입 없이 unseal 되는 구조를 정리한다. 언실 서버는 모노레포의 cone-watcher-openbao-unsealer/(Cloudflare Worker, https://bao-unseal.conecloud.io)이며, 클러스터 밖에 있어서 클러스터 전체가 내려갔다 올라와도 자동 언실이 동작한다.

핵심: 진짜 Vault transit 엔진이 아니라, OpenBao seal "transit" 클라이언트가 호출하는 API 부분집합만 구현한 shim이다. 토큰 갱신(lease) 엔드포인트가 없으므로 OpenBao 쪽 seal 스탠자에 disable_renewal = "true" 가 필수다 (cone-watcher-gitops/argocd/infra/openbao.yaml).

1. 언실 흐름

  • encrypt 는 init/rekey 시점에 root key 블롭을 만들 때 호출된다. 이후 재시작마다 위의 decrypt 만 탄다.
  • 클러스터별로 키 이름과 토큰이 분리되어 있다(autounseal-dev / autounseal-prod). dev 토큰이 유출돼도 prod 키는 못 쓴다.

2. Worker가 구현한 API 표면

엔드포인트인증역할
POST /v1/transit/encrypt/{key}X-Vault-Token{plaintext: b64}{data: {ciphertext: "vault:vN:...", key_version}}
POST /v1/transit/decrypt/{key}X-Vault-Token{ciphertext}{data: {plaintext: b64}}
GET /v1/transit/keys/{key}X-Vault-Token키 메타데이터 (seal 클라이언트 호환용)
GET /v1/sys/health불필요운영 확인용 ({"initialized":true,"sealed":false,...})

에러는 항상 Vault 표준 포맷 {"errors":["..."]} 으로 내려간다 — 이 포맷이어야 seal 클라이언트가 메시지를 그대로 로그에 보여준다.

3. ciphertext 포맷과 키 버전

vault:v{N}:{base64( IV 12바이트 ‖ AES-256-GCM 암호문 )}
  • 암호화는 항상 최신 버전 키, 복호화는 ciphertext에 박힌 vN 버전 키를 쓴다.
  • 그래서 키 로테이션은 UNSEAL_KEYS 에 다음 버전을 추가하고 재배포하면 끝 — 구버전으로 만든 기존 블롭도 계속 풀린다. 구버전 제거는 OpenBao가 새 버전으로 재암호화(재시작 또는 seal rewrap)한 뒤에만 가능하다.

4. 코드 구조 (src/index.ts 단일 파일)

위에서 아래로 읽으면 되는 구조로, HTTP 셸과 암호화 코어가 분리되어 있다.

fetch (진입점)
 └─ try { handle() } catch (HttpError) → {"errors":[...]} 변환   ← 에러 변환은 여기 한 곳뿐
     handle: health → 경로 매칭 → authorize → lookupKeyRing → op별 디스패치

HTTP 셸 (라우팅·인증·요청 파싱)          암호화 코어 (HTTP 모름, 데이터 in/out)
 ├─ authorize        토큰→키이름 권한      ├─ encrypt(ring, b64) → {ciphertext, key_version}
 ├─ lookupKeyRing    키이름→키링           ├─ decrypt(ring, ct)  → {plaintext}
 ├─ requireMethod / requireBodyField       ├─ keyInfo / latestVersion / importKey
 └─ vaultData        {data:...} 포장       └─ b64encode / b64decode
  • 실패는 전부 HttpError(status, message) throw 로 처리한다. 핸들러 곳곳에서 에러 Response를 만들지 않고, fetch 의 catch 한 곳에서만 Vault 에러 포맷으로 변환한다. HttpError 가 아닌 예외(시크릿 설정 오류 등)는 그대로 던져져 Workers가 500으로 처리한다.
  • 암호화 코어 함수들은 Response 를 모른다. 키링(버전 → base64 키)과 데이터만 받아 데이터를 리턴하므로 HTTP 없이 단독 테스트할 수 있다.

5. 시크릿 구조

시크릿위치형태
UNSEAL_KEYSWorkers Secret{"autounseal-dev":{"1":"<b64 32바이트>"},...} — 키이름 → 버전 → AES-256 키
AUTH_TOKENSWorkers Secret{"bao-xxx":["autounseal-dev"],...} — 토큰 → 허용 키이름
openbao-unseal-token각 클러스터 openbao nsOpenBao가 VAULT_TOKEN 으로 주입받는 자기 클러스터용 토큰

UNSEAL_KEYS 를 잃으면 모든 OpenBao를 recovery key로만 복구할 수 있으므로, 키 JSON 원본은 패스워드 매니저에 별도 보관한다.

6. 운영 작업 (상세는 각 문서)

작업문서
최초 설정·배포·스모크 테스트 (task smoke)cone-watcher-openbao-unsealer/README.md
키/토큰 로테이션 절차같은 README의 "키 로테이션"
OpenBao 클러스터 부트스트랩 (init → kv 적재 → ESO)cone-watcher-gitops/docs/openbao.md

빠른 동작 확인:

curl https://bao-unseal.conecloud.io/v1/sys/health
# → {"initialized":true,"sealed":false,"standby":false}