kubectl: x509: certificate signed by unknown authority
안녕하세요, BlueByte입니다. 평범하게 kubectl get pods를 실행했는데 워크로드 대신 Unable to connect to the server: x509: certificate signed by unknown authority가 뜹니다. 클러스터가 죽은 게 아닙니다 — kubectl은 API 서버에 도달해 서버가 제시한 TLS 인증서를 검사했고, 그것을 신뢰하지 못해 거부한 것입니다. 오늘은 메시지 변형들, kubeconfig 안의 certificate authority가 왜 더 이상 맞지 않게 됐는지, 서버가 실제로 보내는 인증서를 보는 법, 원인별 해결, 그리고 재발을 막는 법까지 하나씩 짚어보겠습니다.
kubectl이 실제로 출력하는 x509 오류들
같은 TLS 검사에서 나오는 세 가지 관련 메시지가 있습니다. 가장 흔한 것:
Unable to connect to the server: x509: certificate signed by unknown authoritykubectl이 연결되고, API 서버가 서빙 인증서를 보냈으며, kubectl은 그 인증서를 kubeconfig에 기록된 certificate authority(CA)까지 거슬러 올라가 확인하지 못했습니다. 가까운 친척 둘도 함께 나타납니다:
Unable to connect to the server: x509: certificate has expired or is not yet valid
Unable to connect to the server: x509: certificate is valid for 10.96.0.1, not 192.168.49.2첫 번째는 서버 인증서가 유효 기간을 벗어났다는 뜻이고, 두 번째는 인증서 자체는 진짜지만 여러분이 접속하는 server: 주소와 다른 이름·IP로 발급됐다는 뜻입니다. 세 가지 모두 kubectl이 엔드포인트를 신뢰하지 않겠다는 것이지, 클러스터가 깨진 것은 아닙니다.
CA가 더 이상 맞지 않게 된 이유
"unknown authority" 형태를 만드는 구체적인 원인은 몇 가지입니다:
- 클러스터가 재구축됐거나 인증서가 재생성됨 —
minikube delete && minikube start, 다시 만든 kind 클러스터, 재프로비저닝된 매니지드 클러스터는 모두 새 CA를 발급하지만, kubeconfig에는 여전히 옛certificate-authority-data가 남아 있습니다. - 엉뚱한 클러스터를 가리키고 있음 — 오래된
current-context이거나, 다른 API 서버 앞의 로드 밸런서·프록시로 해석되는server:. - 사내 TLS 검사 프록시가 여러분과 서버 사이에 끼어, 브라우저는 신뢰하지만 kubectl은 신뢰하지 않는 CA로 연결을 다시 서명함.
- self-signed 서버인데 kubeconfig에 CA가 아예 없음 — kubectl이 대조할 대상이 없음.
kubeconfig가 실제로 쓰는 서버와 CA 확인하기
어느 context가 활성인지 추측하지 말고 kubectl에 물어보세요:
kubectl config view --minifyclusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://192.168.49.2:8443
name: minikube
current-context: minikube--minify는 출력을 현재 context 하나로 좁혀, kubectl이 접속하는 정확한 server와 CA가 임베드돼 있는지를 보여줍니다. kubectl config get-contexts는 모든 context를 나열해, 다른 클러스터에서 남은 것이 아니라 의도한 context에 있는지 확인하게 해줍니다.
서버가 실제로 제시하는 인증서 읽기
엔드포인트가 실제로 보내는 것을 OpenSSL로 직접 봅니다:
openssl s_client -connect 192.168.49.2:8443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -datesissuer=CN=kubernetes
subject=CN=kube-apiserver
notBefore=... notAfter=...issuer가 클러스터 CA가 아니라 사내 프록시 CA라면, 경로에 MITM 프록시가 있는 것입니다. notAfter가 과거라면 인증서가 만료된 것입니다. issuer는 맞아 보이는데 kubectl이 여전히 거부한다면, kubeconfig에 임베드된 CA가 오래된 것입니다.
원인별 해결: kubeconfig 갱신 또는 올바른 CA 임베드
재구축·매니지드 클러스터라면 base64를 손으로 고치지 말고 출처에서 새 kubeconfig를 받으세요:
minikube update-context # minikube
aws eks update-kubeconfig --name mycluster # EKS
gcloud container clusters get-credentials mycluster # GKE직접 관리하는 클러스터라면, context가 올바른 CA 파일을 가리키게 하고 설정과 함께 이동하도록 임베드합니다:
kubectl config set-cluster mycluster \
--server=https://192.168.49.2:8443 \
--certificate-authority=/path/to/ca.crt \
--embed-certs=true사내 프록시라면 kubectl이 읽는 번들에 프록시 CA를 추가하거나 API 엔드포인트를 검사에서 우회시키세요 — 검증을 전역으로 끄지는 마세요. 개발 전용 최후 수단으로 검사를 건너뛸 수는 있지만, 그것은 이 오류가 주는 바로 그 보호를 끄는 일임을 이해해야 합니다:
kubectl config set-cluster mycluster --insecure-skip-tls-verify=true실제 사례: 재구축된 minikube 클러스터
망가진 로컬 클러스터를 초기화하려고 minikube delete && minikube start를 실행한 뒤 kubectl get pods가 x509: certificate signed by unknown authority를 던졌습니다. kubectl config view --minify에는 같은 server: https://192.168.49.2:8443이 있어 주소는 문제가 없었습니다 — 다만 새 클러스터의 CA를 캐시된 kubeconfig가 몰랐던 것입니다. minikube update-context가 그 context의 certificate-authority와 클라이언트 인증서를 다시 써 넣었고, 다음 kubectl get pods가 노드 목록을 반환했습니다. 1분도 안 걸렸고, base64 덩어리를 손으로 편집하지 않았습니다.
Unauthorized·connection refused와 어떻게 다른가
x509: ...는 서버의 정체에 관한 것으로, 인증을 받기 전 TLS 핸드셰이크 단계에서 실패합니다. error: You must be logged in to the server (Unauthorized)는 반대입니다: TLS 검사는 통과했지만 여러분의 자격 증명이 거부됐습니다. The connection to the server ... was refused는 더 앞 단계로, API 포트에서 아무도 응답하지 않아 검사할 인증서 자체가 없었던 것입니다. 셋 중 무엇을 받았는지 읽으면 CA를 볼지, 자격 증명을 볼지, 서버가 살아 있는지부터 볼지가 갈립니다.
관련 질문
그냥 --insecure-skip-tls-verify를 넣고 넘어가면 안 되나요?
버리는 로컬 클러스터라면 괜찮습니다. 하지만 그것은 진짜 API 서버와 통신하는지 증명하는 검사를 끄는 것이라, 공유·프로덕션 환경에서는 machine-in-the-middle에 노출됩니다. 대신 CA를 고치세요.
브라우저나 curl은 서버에 닿는데 왜 kubectl은 안 되나요?
브라우저는 OS나 사내 인증서 저장소를 신뢰하지만, kubectl은 kubeconfig 안의 CA만 신뢰합니다. OS가 신뢰하는 프록시 CA는 kubeconfig의 CA에 추가하지 않는 한 kubectl에는 보이지 않습니다.
CA 파일을 갱신했는데도 kubectl이 계속 실패합니다.
흔한 원인 둘: current-context가 다른 클러스터를 가리키거나, 설정에 이미 임베드된 certificate-authority-data(DATA+OMITTED로 표시)가 파일을 덮어씁니다. kubectl config set-cluster를 --embed-certs=true로 다시 실행하고 kubectl config view --minify로 확인하세요.
메시지가 unknown authority가 아니라 certificate has expired입니다.
그건 다른 x509 변형으로, 서버의 서빙 인증서가 유효 기간을 벗어난 것입니다. 갱신하거나(kubeadm 클러스터라면 kubeadm certs renew all), 만료된 것이 클라이언트 인증서라면 kubeconfig를 새로 받으세요.
클러스터를 재구축할 때마다 이 일이 반복되지 않게 하려면?
옛 설정을 기기 간에 복사하지 말고, 클러스터를 다시 만들 때마다 공급자에서 kubeconfig를 재생성하세요(update-context / update-kubeconfig / get-credentials).
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.