Vault Secrets Operator refreshAfter — HMAC 기반 변경 감지

보안3분조회 –

시크릿을 Git 에 평문으로 둘 수는 없으니 값은 Vault 에 두고, Vault Secrets Operator 가 그걸 읽어다 쿠버네티스 Secret 으로 만들어 줍니다. Git 에는 어느 경로에서 읽을지만 남습니다.

이렇게 두면 값이 바뀌었을 때 Git 을 안 고쳐도 됩니다. Vault 에서 값만 바꾸면 오퍼레이터가 다음 주기에 그걸 읽어다 Secret 을 갱신해 주는데, 그 주기를 정하는 게 refreshAfter 입니다.

여기에 refreshAfter: 30s 를 걸어 뒀는데, 30초마다 Vault 를 다시 읽는다는 뜻이라 처음 볼 때는 이래도 되나 싶은 숫자였습니다.

kind: VaultStaticSecret
spec:
  mount: vso-secrets
  path: <namespace>/<app>/.env
  type: kv-v2
  refreshAfter: 30s          # 30초마다 Vault 폴링
  hmacSecretData: true       # 값이 같으면 K8s Secret 을 안 건드림
  destination:
    create: true
    name: <secret-name>
    overwrite: true

refreshAfter 가 Vault 를 얼마나 자주 읽을지를 정하고, hmacSecretData 는 읽은 값이 같을 때 K8s Secret 을 건드릴지를 정하는 것이라, 이름이 나란히 붙어 있어도 하는 일이 다릅니다.

Vault 폴링 자체는 비용 아님

refreshAfter: 30s, VaultStaticSecret 10개 기준으로 계산하면 이렇습니다.

분당  10개 × 2회 = 20 호출
하루  28,800 호출

초당으로 치면 0.3 회라, 부하라고 부를 수준이 아닙니다.

진짜 비용은 Kubernetes Secret 쓰기

Secret 을 쓸 때마다 etcd 에 쓰기가 일어나고 그 Secret 을 보고 있는 watch 로 전부 전파되니, Vault 를 한 번 더 읽는 것과는 비용이 다릅니다. hmacSecretData: true 가 하는 일이 여기서 나옵니다 — 30초마다 Vault 를 읽지만 값이 안 바뀌었으면 Kubernetes Secret 을 갱신하지 않습니다.

1. 30초마다 Vault kv-v2에서 현재 값 읽기
2. 읽어온 값의 HMAC 계산
3. 이전에 저장한 HMAC과 비교
4. 같으면: 아무것도 안 함 (K8s API 호출 없음)
5. 다르면: K8s Secret 업데이트

비교에 쓰는 해시는 리소스 상태에 남아 있어서 직접 꺼내 볼 수 있습니다.

kubectl get vaultstaticsecret <vss-name> -n <namespace> \
  -o jsonpath='{.status.secretMAC}'

hmacSecretData 가 없으면 값이 같아도 매번 Secret 을 덮어쓰니 불필요한 Kubernetes API write 가 생기고, rolloutRestartTargets 를 걸어 뒀다면 그때마다 파드가 다시 뜹니다. 30초마다 시크릿을 다시 읽는 구성이 아니라 30초마다 파드가 재시작하는 구성이 되는 겁니다.

30초가 항상 맞는 값은 아님

시크릿을 바꿨을 때 빠르게 반영돼야 하는 워크로드라 30초로 잡았습니다. 반영이 몇 분 늦어도 되는 서비스면 주기를 늘려도 되지만, 늘려서 얻는 건 크지 않습니다.

폴링 주기를 줄일지 정하는 기준은 Vault 부하가 아니라 얼마나 빨리 반영돼야 하는가 쪽입니다. 부하 쪽은 이미 hmacSecretData 가 막고 있습니다.

참고

  1. 불러오는 중