IT/DevSecOps

Vault Agent sidecar의 init 컨테이너 캐시 공유 함정

gfrog 2026. 6. 25. 09:46
SMALL

지지난 주에 우리 팀이 운영하는 결제 도메인 클러스터에서 Vault 토큰 관련 알람이 30분 가까이 폭주했다. P99 레이턴시도 같이 튀었고, 무엇보다 새벽 2시였다. 결론부터 말하면 Vault Agent injector가 만든 sidecar가 init 컨테이너가 이미 받아둔 토큰을 재사용한다고 믿고 있었는데, 실제로는 그렇지 않아서 매번 다시 인증을 시도하다가 Vault 서버 쪽 rate limit에 걸린 사건이었다.

좀 더 풀어서 적어둔다. 같은 함정 밟는 분 있을지도 모르니까.

어떻게 발견했는가

알람은 단순했다. vault-agent 컨테이너에서 permission denied429 too many requests가 번갈아 찍히고 있었다. 처음에는 정책(vault policy) 문제인가 싶었다. 그런데 토큰을 직접 발급해서 같은 정책으로 조회해보면 멀쩡히 동작했다.

이상한 건, 알람이 터지기 직전에 진행한 작업이 Deployment의 replica 수를 12개에서 48개로 늘린 것뿐이라는 거였다. HPA가 한꺼번에 스케일을 잡아당기면서 Pod 36개가 거의 동시에 떴다. 그리고 그 Pod들이 전부 Vault에 인증 요청을 보냈다.

2026-06-1X 02:11:37 [INFO]  auth.handler: authenticating
2026-06-1X 02:11:37 [WARN]  auth.handler: auth request failed: 429 Too Many Requests
2026-06-1X 02:11:42 [INFO]  auth.handler: retrying after backoff

이때만 해도 "스케일 한번에 너무 많이 했나" 정도로 끝낼 뻔했다. 근데 일과시간에 다시 보니 한 가지가 더 보였다. 각 Pod 안에서 init 컨테이너가 이미 토큰을 받았고, sidecar가 동일한 토큰을 재사용했어야 하는데 그게 안 되고 있었다.

init 컨테이너와 sidecar는 다른 프로세스다

이 부분이 핵심이었다. Vault Agent Injector를 쓰면 보통 이런 구조가 된다.

  • init 컨테이너 vault-agent-init: Pod 시작 시 Vault에 한 번 인증해서 시크릿을 /vault/secrets/ 로 떨어뜨리고 종료
  • sidecar 컨테이너 vault-agent: Pod이 살아있는 동안 백그라운드에서 토큰을 갱신하고 시크릿을 다시 써줌

문제는, init 컨테이너가 종료되면서 메모리에 들고 있던 토큰도 같이 사라진다는 점이다. sidecar는 별도 프로세스이기 때문에, init가 받아둔 토큰을 어디선가 다시 읽어와야 한다. 우리는 그게 자동으로 되는 줄 알았다.

답은 "그렇게 만들고 싶으면 persistent cache를 직접 설정해야 한다"였다. 그렇지 않으면 sidecar는 그냥 시작할 때 한 번 더 Vault에 인증을 한다. Kubernetes auth method 기준으로 보면, 각 Pod이 두 번씩 service account JWT를 들고 Vault에 인증을 하는 셈이다.

평소엔 문제가 안 됐다. Pod이 100개여도 분산되어 있으니까. 그런데 36개가 동시에 뜨면 72번의 인증 요청이 거의 같은 순간 Vault 서버에 박힌다. 그리고 우리 Vault는 rate_limit_quota가 분당 60건으로 잡혀 있었다. 충돌은 시간 문제였다.

왜 이걸 이제야 알았나

솔직히 부끄러운 얘긴데, 우리는 Vault Agent 도입할 때 HashiCorp 공식 튜토리얼을 거의 그대로 따라했다. 거기엔 persistent cache 설정이 들어있지 않다. 트래픽이 적은 단일 Pod 데모 기준으로 잘 동작하기 때문이다.

GitHub에 가보니 이슈가 있긴 했다. vault-agent-in-sidecar does not reuse from persistent cache the token fetched by the initcontainer 라는 제목. 우리가 겪은 것과 정확히 같은 패턴이었다. persistent cache를 설정해도 sidecar가 init의 토큰을 재사용하지 않고 처음부터 다시 인증한다는 보고였다.

읽다 보니 좀 복잡했다. cache는 만들 수 있는데, sidecar가 그 캐시를 init의 결과물로 인식할지는 별개라는 거다. 토큰을 파일로 떨어뜨리고 sidecar가 그걸 읽어서 lookup-self로 검증한 다음 재사용하도록 명시적으로 구성해야 안전했다.

우리가 적용한 방식

# init과 sidecar가 공통으로 사용
auto_auth {
  method "kubernetes" {
    mount_path = "auth/kubernetes"
    config = {
      role = "payment-app"
      token_file_path = "/vault/token/.vault-token"
    }
  }

  sink "file" {
    config = {
      path = "/vault/token/.vault-token"
    }
  }
}

cache {
  use_auto_auth_token = true
  persist {
    type = "kubernetes"
    path = "/vault/agent-cache"
    keep_after_import = true
    exit_on_err = false
  }
}

핵심은 두 개다.

  1. sink "file"로 init이 받은 토큰을 emptyDir 볼륨에 떨어뜨린다. sidecar가 동일한 볼륨을 마운트해서 그 파일을 token_file_path로 읽는다.
  2. persist로 캐시를 Kubernetes secret 백엔드에 저장한다. sidecar 재시작 시에도 살아있다.

Helm chart의 vault.hashicorp.com/agent-cache-enable: "true" annotation으로도 비슷한 효과를 낼 수 있다는데, 우리는 어떤 옵션이 어떻게 매핑되는지 추적이 어려워서 ConfigMap으로 직접 떨어뜨리는 방식을 택했다. 운영 관점에서는 이쪽이 디버깅이 쉬웠다.

그래도 남은 의문

지금 구조가 정답이라고는 말 못 하겠다. 토큰을 emptyDir에 파일로 떨어뜨리는 게 보안 관점에서 마음에 걸린다. 같은 Pod 안의 다른 컨테이너에서 그 파일을 읽을 수 있다는 얘기다. 우리 결제 서비스는 단일 앱 컨테이너만 있어서 문제가 안 됐지만, sidecar가 여러 개 들어있는 환경에서는 다시 생각해봐야 한다.

또 하나, Vault 쪽 rate limit을 분당 60건에서 300건으로 올렸다. 임시방편이긴 한데, 도메인이 늘어나면 한계가 올 거다. 장기적으로는 Vault Secrets Operator를 보고 있다. CRD 기반으로 시크릿을 동기화하는 방식이라 Pod마다 인증하는 부담이 없다. 다만 마이그레이션 비용이 작지 않아서 다음 분기 일정에 넣어둔 상태.

비슷한 거 운영하시는 분 있으면 어떻게 풀고 계신지 댓글로 알려주시면 감사하겠습니다. 특히 멀티 클러스터 환경에서.

추가 리소스

BIG