CronJob JWKS 유실 — 이미지에 없는 도구와 set -e

Kubernetes3분조회 –

어느 날부터 앞단 게이트웨이에서 JWT 검증이 실패하기 시작했습니다.

kid not found in jwks

토큰을 검증하려면 발급자 공개키가 있어야 하고, 발급자는 키를 주기적으로 갈아 끼웁니다. 그래서 발급자 OIDC 엔드포인트에서 키 묶음(JWKS)을 주기적으로 받아다 ConfigMap 에 넣어 두고, 게이트웨이는 그 ConfigMap 을 읽어 갑니다. kid not found 는 토큰 헤더의 kid(어떤 키로 서명했는지 가리키는 식별자)에 해당하는 키를 그 묶음에서 못 찾았다는 뜻입니다.

ConfigMap 을 열어 보니 발급자 쪽 OIDC 엔드포인트는 키 5개를 서빙하고 있지만 게이트웨이가 읽는 쪽에는 그중 아무것도 없었습니다. 사람이 지운 게 아니라 갱신하는 쪽에서 빈 값을 써넣고 있었습니다.

원인 — 베이스 이미지에 없는 python3

JWKS 를 주기적으로 받아 ConfigMap 을 갱신하는 CronJob 이 있었고, 받은 JSON 을 --from-literal 에 넘기기 좋게 이스케이프하려고 중간에 python 을 끼워 뒀습니다.

JWKS=$(curl -s "$OIDC_URL/keys" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')
kubectl create configmap jwks --from-literal=jwks.json="$JWKS" ...

이 Job 이 쓰는 이미지는 bitnami/kubectl 이고, 여기에는 python3 이 없습니다. 그러니 저 줄은 python3: command not found 로 실패하고 JWKS 는 빈 값이 됩니다.

진짜 문제 — set -e 가 없어 안 멈추는 스크립트

명령어가 없어서 실패한 것보다 스크립트가 거기서 안 멈춘 게 더 문제였습니다. set -e 가 없어서 JWKS 가 비어도 다음 줄이 그대로 실행됐고 빈 값으로 ConfigMap 을 덮어썼습니다. 마지막 kubectl create 는 성공했으니 CronJob 은 Completed 로 끝나고 모니터링에서는 매번 정상적으로 갱신되는 것처럼 보입니다.

실패가 에러로 드러나는 게 아니라 정상 완료로 기록되니, 게이트웨이가 토큰을 거부하기 전까지는 알 방법이 없었습니다.

해결 — 있는 도구만 쓰고 크게 실패시키기

python 을 걷어내고 kubectl 이 이미 할 줄 아는 방식으로 바꿨습니다. 이스케이프는 --from-literal 로 만든 매니페스트를 --dry-run=client -o yaml 로 뽑아 그대로 kubectl apply 에 넘기면 해결됩니다.

set -e
JWKS=$(curl -sf "$OIDC_URL/keys")
kubectl create configmap jwks \
  --from-literal=jwks.json="$JWKS" \
  --dry-run=client -o yaml | kubectl apply -f -

python3 을 걷어내 이미지에 있는 도구만 쓰게 하고, set -e 와 curl -f 로 중간에 실패하면 즉시 멈추게 했습니다. 이제 갱신이 실패하면 CronJob 이 실패로 뜨고 ConfigMap 에는 마지막으로 성공했던 값이 그대로 남습니다.

조용한 실패가 나오는 두 지점

이미지에 무엇이 들어 있는지는 만든 사람 마음이라, 당연히 있으려니 하고 쓴 도구는 그 줄에서만 조용히 실패합니다. 스크립트를 짜기 전에 그 이미지에서 command -v 로 쓰려는 도구가 있는지 확인하면 여기서 걸립니다.

set -e 없이 짠 파이프라인은 중간이 깨져도 끝까지 달려서 멀쩡한 데이터를 빈 값으로 덮습니다. 갱신 작업은 성공하는 것보다 실패했을 때 기존 값을 안 건드리는 쪽이 중요하니 새 값을 쓰는 스크립트에는 set -e 부터 넣고 시작합니다.

  1. 불러오는 중