가끔 장애 분석은 탐정물보다 더 재미있다.

분명 출입증은 있다. 문도 보인다. 경비원도 일하고 있다. 그런데 입구에서 이런 말을 듣는다.

이 출입증은 이 건물용이 아닌데요?

이번 이야기는 Kubernetes kubeconfig와 TLS 인증서가 서로 다른 주소를 들고 있어서 생긴 작은 미스터리다.

처음 보면 꽤 무섭다.

kubectl이 접속을 못 한다. 인증서 오류가 난다. 누군가는 insecure-skip-tls-verify: true를 넣으니 된다고 말한다. 그러면 머릿속에 바로 빨간불이 켜진다.

“어? 이거 보안 우회야?”

맞기도 하고, 아니기도 하다.

정확히 말하면 “제품의 인증/인가 우회”는 아니고, “TLS 서버 신원 검증 우회”다. 이 차이를 놓치면 원인을 엉뚱한 층에서 찾게 된다.

kubeconfig는 출입증이다

먼저 kubeconfig를 쉽게 보자.

kubectl은 Kubernetes에게 말을 거는 리모컨이다.

kubectl get ns

이 명령은 대략 이런 뜻이다.

쿠버네티스야, 네임스페이스 목록 보여줘.

그런데 kubectl은 혼자서는 어디로 가야 하는지 모른다. 그래서 kubeconfig라는 설정 파일을 본다.

거기에는 이런 정보가 들어 있다.

clusters:
- cluster:
    server: https://example.com:6443
    certificate-authority-data: ...

사람 말로 바꾸면 이렇다.

건물 주소: https://example.com:6443
이 건물이 진짜인지 확인할 기준: certificate-authority-data

server는 찾아갈 주소다.

certificate-authority-data는 “이 문패가 진짜인지 확인하는 기준”이다.

TLS 인증서는 문패다

TLS 인증서는 서버가 내미는 문패다.

브라우저에서 https:// 사이트에 접속할 때도 비슷한 일이 일어난다. 브라우저는 서버에게 묻는다.

너 정말 example.com 맞아?

서버는 인증서를 내민다.

내 이름은 example.com이야.
이 CA가 나를 보증해.

여기서 중요한 항목이 SAN이다.

SAN은 Subject Alternative Name의 줄임말이다. 쉽게 말하면 “이 인증서가 대표할 수 있는 이름 목록”이다.

예를 들어 인증서 SAN에 이런 값이 있다고 하자.

DNS:internal.example.local
IP Address:10.0.0.7
IP Address:127.0.0.1

그런데 kubectl이 이렇게 접속한다.

server: https://public.example.com:6443

그러면 kubectl은 이렇게 말한다.

나는 public.example.com으로 왔는데,
네 문패에는 internal.example.local만 적혀 있네?
이 건물이 맞다는 걸 믿을 수 없어.

그래서 접속을 막는다.

이건 버그라기보다 TLS의 정상적인 방어 동작이다.

문제는 “어느 인증서냐”다

Kubernetes 접근 제어 제품을 다루다 보면 TLS 구간이 하나가 아니다.

대략 두 개가 있다.

1. kubectl -> 접근 제어 프록시
2. 접근 제어 프록시 -> Kubernetes API Server

둘 다 TLS를 쓴다.
둘 다 인증서가 있다.
둘 다 SAN mismatch가 날 수 있다.

그래서 질문을 먼저 나눠야 한다.

지금 실패한 인증서는 누구의 인증서인가?

첫 번째 구간이면 사용자의 kubectl이 프록시 서버의 인증서를 검증하다가 실패한 것이다.

두 번째 구간이면 제품 서버가 Kubernetes API Server의 인증서를 검증하다가 실패한 것이다.

둘은 전혀 다른 문제다.

마치 회사 정문에서 막힌 것과, 회사 안쪽 서버실 문에서 막힌 것을 구분해야 하는 것과 같다.

이번 퍼즐의 핵심: 출입증 주소는 새 건물, 문패는 옛 건물

상황을 단순화하면 이랬다.

kubeconfig server:
  https://public-ip:6443

실제 프록시 서버 인증서 SAN:
  127.0.0.1
  ::1
  private-ip

kubectl 입장에서는 당연히 이상하다.

나는 public-ip로 왔는데, 서버가 내민 문패에는 private-ip만 적혀 있다.

그래서 이런 오류가 난다.

x509: certificate is valid for 127.0.0.1, ::1, private-ip, not public-ip

재미있는 건 여기서부터다.

소스 구조를 따라가 보니, kubeconfigserver와 프록시 인증서의 SAN은 원칙적으로 같은 설정에서 나온다.

흐름은 대략 이렇다.

프록시 설정의 host/port
  -> kubeconfig server

프록시 설정의 host/port
  -> 프록시 기동 시 인증서 SAN 생성

정상이라면 둘이 같아야 한다.

그런데 실제로는 달랐다.

그럼 가장 그럴듯한 시나리오는 이거다.

1. 처음에는 프록시 주소가 private-ip였다.
2. 이때 프록시가 기동되며 private-ip가 들어간 인증서를 만들었다.
3. 나중에 외부 접속을 위해 주소가 public-ip로 바뀌었다.
4. 새 kubeconfig는 public-ip를 받았다.
5. 하지만 프록시는 재기동되지 않아 예전 인증서를 계속 들고 있었다.

즉 출입증에는 새 주소가 찍혔는데, 건물 문패는 아직 옛 주소인 셈이다.

insecure-skip-tls-verify는 만능열쇠가 아니다

이때 누군가 이렇게 말할 수 있다.

insecure-skip-tls-verify: true

이걸 넣으니 접속이 된다고.

맞다. 될 수 있다.

하지만 이 옵션이 하는 일은 명확하다.

서버가 진짜 그 서버인지 확인하지 않겠다.

문패 확인을 건너뛰는 것이다.

중요한 점은 이게 Kubernetes 권한 검사를 없애는 옵션은 아니라는 점이다. 접근 제어 프록시가 토큰을 확인하고, 역할을 보고, 정책을 적용하고, 감사 로그를 남기는 흐름은 계속 동작할 수 있다.

그래서 이 옵션을 “제품 인증 우회”라고 부르면 틀릴 수 있다.

더 정확한 표현은 이거다.

TLS 서버 신원 검증 우회

하지만 그렇다고 안전한 해결책이라는 뜻은 아니다.

서버 신원 검증을 끄면 중간자 공격에 약해진다. 운영 환경에서는 “잠깐 확인용 임시 우회” 정도로만 봐야 한다.

정식 해결은 문패와 출입증의 주소를 맞추는 것이다.

확인은 세 단계면 충분하다

이런 이슈를 보면 먼저 세 가지를 확인하면 좋다.

1. 현재 설정 주소 확인

프록시 설정에 어떤 host/port가 들어 있는지 본다.

SELECT uuid, name, host, port, created_at
FROM proxy_setting_table;

여기서 봐야 할 것은 간단하다.

kubeconfig server에 적힌 주소와 같은가?

2. 실제 서버 인증서 SAN 확인

프록시 6443 포트가 실제로 어떤 인증서를 내미는지 본다.

openssl s_client -connect <proxy-host>:6443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -text -noout | grep -A10 "Subject Alternative Name"

이 명령은 “서버가 내미는 문패에 어떤 이름들이 적혀 있는지” 보는 도구다.

기대하는 결과는 단순하다.

kubeconfig server의 host/IP가 SAN 안에 있어야 한다.

3. 프록시가 언제 기동됐는지 확인

인증서가 기동 시점에 만들어지는 구조라면, 프로세스나 Pod의 시작 시간이 중요하다.

kubectl get pod -A -o wide | grep -i proxy
kubectl describe pod <proxy-pod> -n <namespace>

또는 Docker라면 이런 식이다.

docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'

주소를 바꾼 시각보다 프록시 기동 시각이 더 오래됐다면, 예전 문패를 계속 들고 있을 가능성이 커진다.

좋은 답변은 “버그냐 아니냐”보다 “어느 층이냐”를 먼저 말한다

이런 티켓에서 바로 “제품 버그입니다” 또는 “고객 환경 문제입니다”라고 말하면 위험하다.

더 좋은 답변은 층을 나누는 것이다.

이번 증상은 Kubernetes API Server로 가는 인증/권한 검증 단계가 아니라,
사용자의 kubectl이 접근 제어 프록시 서버 인증서를 검증하는 단계에서 발생한 SAN 불일치로 보입니다.

그리고 다음을 붙인다.

insecure-skip-tls-verify는 접속 확인용 임시 우회로는 동작할 수 있지만,
서버 신원 검증을 생략하므로 정식 운영 조치로 권장하지 않습니다.

마지막으로 정식 조치를 말한다.

프록시 접속 주소와 프록시 서버 인증서 SAN이 일치하도록 설정과 재기동 상태를 확인해야 합니다.

이렇게 말하면 상대방도 “어디를 봐야 하는지” 알 수 있다.

이 작은 사건이 알려준 것

이번 퍼즐은 결국 아주 단순한 교훈으로 돌아온다.

주소와 문패는 같아야 한다.

하지만 시스템 안에서는 이 단순한 사실이 자주 흐려진다.

설정은 최신인데, 프로세스는 예전 설정으로 떠 있을 수 있다.

출입증은 새로 발급됐는데, 문패는 옛날 그대로일 수 있다.

오류 메시지는 “인증서가 틀렸다”고 말하지만, 실제 원인은 “인증서를 만든 시점과 설정을 읽은 시점이 다르다”일 수 있다.

그래서 디버깅할 때는 이렇게 물어보면 좋다.

이 값은 어디서 왔지?
언제 읽혔지?
지금도 다시 읽히나?
아니면 시작할 때 한 번만 읽히나?

이 네 질문만으로도 많은 미스터리가 풀린다.

그리고 마지막으로 하나 더.

insecure라는 이름이 붙은 옵션은 대부분 이름값을 한다.

되는 것과 안전한 것은 다르다.

잠깐 문을 열어볼 수는 있다. 하지만 운영 환경에서는 문패를 고치는 게 맞다.