SealedSecret이 못 남기는 것 - OpenBao + External Secrets Operator로 감사 로그 확보하기
Git에 커밋 가능한 SealedSecret과, 회전·감사가 필요한 값을 위한 중앙 시크릿 저장소 OpenBao + External Secrets Operator(ESO). 언제 어느 쪽을 쓸지 기준을 정리하고, 개인 랩 규모에서 실제로 내린 반대 방향 판단까지.
목표
시리즈 6(ISMS-P 매핑)에서 남겨둔 숙제 하나 - SealedSecret은 "평문이 Git에 올라가지 않는다"는 것까지는 보장하지만, 누가 언제 그 시크릿을 열람·변경했는지에 대한 감사 로그를 남기지 않는다. 이 갭을 메우는 표준적인 방법(OpenBao + External Secrets Operator)을 실제로 구성해보고, 이 랩 규모에서는 그 방법을 실제로 도입할 가치가 있는지까지 따져본다.
개념: 시크릿마다 "진실 공급원"이 다르다
시리즈 5에서 Keycloak 클라이언트 시크릿을 SealedSecret으로 풀었다 - kubeseal로 암호화해서 Git에 커밋하고, 클러스터 안의 컨트롤러만 복호화할 수 있는 방식이다. 값이 거의 안 바뀌고 Git으로 이력 관리가 되면 충분한 시크릿에는 이 방식이 잘 맞는다.
반면 DB 접속 정보, 외부 API 키, 서비스 간 통신 토큰처럼 회전(rotation)이 필요하거나, 여러 서비스가 같은 값을 공유해야 하거나, 값 자체를 감사(audit)해야 하는 시크릿은 성격이 다르다. SealedSecret은 "이미 암호화된 값을 Git에 안전하게 두는 것"에는 강하지만, 그 값 자체를 중앙에서 관리하고 회전 이력을 추적하는 용도로는 설계되어 있지 않다.
OpenBao(HashiCorp Vault 계열의 오픈소스 시크릿 관리 도구)는 시크릿을 중앙에 저장하고, 누가 언제 어떤 시크릿에 접근했는지 감사 로그를 남기며, 주기적인 자동 회전까지 지원하는 전용 시스템이다. 문제는 서비스가 이 저장소를 직접 호출하게 만들면 코드가 특정 도구에 종속된다는 것 - 그래서 **External Secrets Operator(ESO)**가 중간에서 OpenBao의 값을 읽어 평범한 Kubernetes Secret으로 동기화해준다. 서비스 입장에서는 여전히 일반 Secret을 쓰는 것과 다르지 않다.
[ OpenBao ] ← 시크릿의 단일 진실 공급원, 회전·감사 이력 관리
│ ExternalSecret 리소스가 참조
▼
[ External Secrets Operator ]
│ 주기적으로 동기화
▼
[ 네임스페이스별 Kubernetes Secret ]
│ 일반 Secret처럼 마운트/환경변수로 주입
▼
[ 서비스 파드 ]
실제 구축 절차
1. OpenBao + ESO 설치
helm repo add openbao https://openbao.github.io/openbao-helm
helm install openbao openbao/openbao --namespace security --create-namespace
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets --namespace security
2. SecretStore / ExternalSecret 연결
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: openbao-backend
namespace: app
spec:
provider:
vault:
server: "http://openbao.security:8200"
path: "secret"
version: "v2"
auth:
tokenSecretRef:
name: openbao-token
key: token
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-server-db-secret
namespace: app
spec:
secretStoreRef: {name: openbao-backend, kind: SecretStore}
target: {name: api-server-db-secret}
refreshInterval: 1h
data:
- secretKey: DB_PASSWORD
remoteRef: {key: app/db, property: password}
위 예시는 토큰 기반 인증이지만, 운영 환경에서는 정적 토큰을 또 하나의 시크릿으로 관리해야 하는 문제를 피하려고 쿠버네티스 ServiceAccount 인증 방식을 쓰는 경우가 많다 - ESO가 자신의 ServiceAccount 토큰을 제시하면 OpenBao가 역할(role)에 매핑해서 접근을 허용하는 방식이다. 정적 크리덴셜을 하나 더 만드는 대신, 이미 있는 쿠버네티스 신원 체계를 그대로 재사용하는 셈이다.
refreshInterval도 시크릿 성격에 따라 다르게 잡을 수 있다 - 자주 참조되고 회전 가능성이 있는 값은 짧은 주기(분 단위), 거의 안 바뀌는 값은 긴 주기(시간 단위)로 두면 된다. 전부 짧게 잡으면 OpenBao에 불필요한 폴링 부하가 쌓이고, 전부 길게 잡으면 회전이 반영되는 데 오래 걸린다.
OpenBao 자체를 어떻게 고가용성으로 두나
모든 시크릿이 한 곳으로 모이는 구조라, OpenBao 하나가 단일 장애점이 되면 "시크릿 중앙화"가 오히려 리스크를 키우는 셈이 된다. Raft 기반 스토리지로 여러 인스턴스(3노드 등)가 복제·리더 선출을 하도록 구성하면, 하나가 죽어도 나머지가 리더를 이어받아 서비스가 끊기지 않는다. 다만 이건 프로덕션 규모에서 감수할 만한 운영 부담이지, 개인 랩에서 굳이 처음부터 갖출 필요는 없다 - 아래 아코디언 참고.
3. 검증
# 1) OpenBao에서 값 변경
bao kv put secret/app/db password=<새 값>
# 2) ESO 다음 동기화 주기 후 K8s Secret이 자동 갱신됐는지 확인
kubectl get secret api-server-db-secret -n app -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# 3) OpenBao 감사 로그(audit log) 활성화 후, 방금 그 조회가 기록되는지 확인
bao audit enable file file_path=/vault/logs/audit.log
"누가 이 시크릿을 읽었는지"가 감사 로그에 실제로 남는다는 것 - 이게 SealedSecret으로는 만들 수 없었던 증적이다.
그런데 이 랩에서는 실제로 반대 방향(SealedSecret 유지)을 택했다
위 구성은 "OpenBao+ESO가 SealedSecret보다 나은 지점"을 보여주기 위한 절차다. 그런데 이 랩의 GitHub 저장소를 public으로 전환할지 검토하는 과정에서 실제로 내린 판단은 정반대였다 - ESO 도입은 보류하고 SealedSecret 체계를 유지하기로 했다.
이유는 단순하다. ESO는 결국 OpenBao 같은 시크릿 저장소를 어딘가에 떠 있는 상태로 계속 운영해야 한다는 뜻이다(이 랩에서라면 클러스터 안의 OpenBao 자체도 별도로 운영·백업해야 하는 컴포넌트다). 개인 학습 랩 규모에서는 회전·감사가 필요한 시크릿 자체가 거의 없는데, 그 이점을 위해 컴포넌트 하나를 통째로 더 짊어지는 건 배보다 배꼽이 큰 선택이라고 판단했다. SealedSecret은 추가 인프라 없이 "암호화해서 Git에 커밋"만으로 끝나서, 이 랩이 실제로 겪고 있던 문제(ArgoCD/Harbor 비밀번호가 작업 기록에 평문으로 남은 것) - 즉 "평문이 저장소에 있으면 안 된다" - 를 해결하는 데는 이걸로 충분했다.
같은 도구를 두고 프로덕션과 학습 랩에서 정반대 결론이 나온 이유는 결국 규모와 시크릿의 성격이다. 서비스가 여러 개로 늘고 회전이 필요한 크리덴셜이 실제로 존재하는 프로덕션에서는 OpenBao+ESO의 운영 비용을 감수할 값어치가 있지만, 서비스 몇 개짜리 개인 랩에서는 그 비용을 감당할 이유가 없었다 - "더 좋은 도구"가 아니라 **"지금 이 규모에 맞는 도구"**가 기준이었다.
| SealedSecret | OpenBao + ESO | |
|---|---|---|
| 진실 공급원 | Git (암호화된 형태로) | OpenBao |
| 적합한 경우 | 값이 거의 안 바뀌는 서비스별 시크릿, 개인/소규모 랩 | 회전이 필요하거나 여러 서비스가 공유하는 값, 감사가 요구되는 환경 |
| 추가 인프라 | 없음 (컨트롤러 하나) | OpenBao 자체를 별도로 운영·백업 필요 |
| 이력 추적 | Git 커밋 이력 | OpenBao 자체 감사 로그 |
검증 방법
- OpenBao에서 값을 바꾼 뒤, ExternalSecret의
refreshInterval이 지나면 K8s Secret이 실제로 갱신되는지 확인 - 감사 로그를 켠 상태에서 시크릿을 조회하고, 그 조회 기록이 감사 로그에 남는지 확인
- (이 랩처럼 도입하지 않기로 했다면) 왜 안 했는지를 위 표처럼 트레이드오프로 설명할 수 있는지 - "안 했다"가 아니라 "왜 안 했는지"가 증적이자 판단 근거가 된다
체크리스트
- OpenBao + External Secrets Operator 설치, SecretStore/ExternalSecret 연결 확인
- 감사 로그 활성화 + 시크릿 접근 기록 확인
- SealedSecret과 OpenBao+ESO 중 무엇을 쓸지, 이 랩/프로젝트 규모에 맞는 기준으로 판단하고 근거 문서화