# [k8s hard way] 03-CA와 인증서로 컴포넌트 신뢰 세우기
02-Jumpbox 설정 (opens new window)에서 도구를 준비했습니다. 다음 단계는 인증서를 만드는 것인데, 이 단계가 튜토리얼에서 가장 기계적으로 보이면서 실제로는 클러스터 권한 모델 전체를 결정합니다.
명령을 따라 치면 파일이 만들어지지만, 왜 인증서마다 다른 값을 넣는지를 모른 채 지나가면 나중에 "kubelet이 왜 이건 못 읽지"에서 막힙니다. 이 글에서는 인증서의 어떤 필드가 클러스터의 어떤 권한이 되는지 정리합니다.
# 1. 왜 인증서인가
Kubernetes 컴포넌트들은 서로를 HTTPS로 부릅니다. kubelet이 API 서버를 부르고, API 서버가 kubelet을 부르고, 스케줄러와 컨트롤러 매니저도 API 서버를 부릅니다.
여기서 필요한 것이 둘입니다.
- 상대가 맞는지 확인 - 서버 인증서
- 내가 누구인지 증명 - 클라이언트 인증서
관리형 클러스터에서는 이 과정이 보이지 않습니다. 직접 세우면 인증서를 만드는 행위가 곧 계정을 만드는 행위라는 것이 드러납니다.
# 2. 자체 서명 CA 하나로 시작한다
Every certificate authority starts with a private key and root certificate.
# 4096비트 키와 10년짜리 루트 인증서
openssl genrsa -out ca.key 4096
openssl req -x509 -new -sha512 -noenc \
-key ca.key -days 3653 \
-config ca.conf \
-out ca.crt
이 CA가 서명한 인증서를 클러스터가 신뢰합니다. 즉 ca.key를 가진 사람은 아무 신원의 인증서나 발급할 수 있습니다. system:masters 그룹이 박힌 인증서를 만들면 그 순간 클러스터 관리자가 됩니다.
튜토리얼도 이 점을 짚습니다.
In a real-world environment these certificates should be treated like a set of sensitive secrets as they are used as credentials by the Kubernetes components to authenticate to each other.
CA 개인키의 보관이 클러스터 보안의 최상위 항목입니다. 그리고 인증서 기반 인증에는 폐기 목록이 사실상 없습니다. 유출된 클라이언트 인증서를 무효화하려면 CA를 갈고 전부 재발급해야 합니다.
# 3. 여덟 개의 인증서, 여덟 개의 신원
만드는 인증서는 이렇습니다.
| 인증서 | 쓰는 곳 | 역할 |
|---|---|---|
admin | 사람의 kubectl | 관리자 클라이언트 |
node-0, node-1 | 각 워커의 kubelet | 노드 클라이언트 |
kube-proxy | 각 노드의 kube-proxy | 클라이언트 |
kube-scheduler | 스케줄러 | 클라이언트 |
kube-controller-manager | 컨트롤러 매니저 | 클라이언트 |
kube-api-server | API 서버 | 서버 |
service-accounts | 토큰 서명 | 서명 키 |
앞의 여섯은 클라이언트, kube-api-server는 서버, service-accounts는 성격이 다릅니다. 마지막 것은 통신에 쓰이지 않고 ServiceAccount 토큰에 서명하는 키쌍입니다. API 서버는 개인키로 TokenRequest 토큰에 서명하고(--service-account-signing-key-file) 공개키로 토큰을 검증하며(--service-account-key-file), 컨트롤러 매니저도 같은 개인키로 레거시 Secret 기반 토큰에 서명하므로(--service-account-private-key-file) 컨트롤러 매니저와 API 서버가 짝을 맞춰 들고 있어야 합니다.
# 4. CN과 O가 사용자와 그룹이 된다
이 단계의 핵심입니다. Kubernetes는 클라이언트 인증서에서 두 필드를 읽어 신원으로 씁니다.
- CN (Common Name) → 사용자 이름
- O (Organization) → 그룹 (여러 개 가능)
그래서 인증서를 발급하는 것이 곧 사용자를 만드는 것입니다. 별도의 사용자 등록 API가 없는 이유이기도 합니다.
노드 인증서가 가장 명확한 예입니다. Kubernetes 문서가 요구 사항을 못 박아 둡니다.
Kubelets must use a credential that identifies them as being in the
system:nodesgroup, with a username ofsystem:node:<nodeName>.
The
<nodeName>value must match precisely the name of the node as registered by the kubelet.
즉 node-0의 인증서는 이렇게 만들어야 합니다.
CN = system:node:node-0
O = system:nodes
두 값이 모두 정확해야 합니다. 그리고 <nodeName>이 kubelet이 등록하는 노드 이름과 글자 단위로 같아야 합니다. 호스트명을 바꾸거나 --hostname-override를 주면 이 값이 어긋나고, kubelet은 인증은 되는데 인가에서 막힙니다. 오류가 401이 아니라 403으로 나오므로 원인이 인증서라는 것을 알아채기 어렵습니다.
다른 컴포넌트도 이름이 정해져 있습니다. system:kube-scheduler, system:kube-controller-manager, system:kube-proxy 같은 이름은 클러스터에 기본 내장된 ClusterRoleBinding이 가리키는 대상입니다. 임의로 짓지 않고 정해진 이름을 써야 기본 권한이 붙습니다.
# 5. Node 인가는 인증서 없이는 성립하지 않는다
CN/O를 정확히 맞춰야 하는 이유가 인가 모드에 있습니다. Node 인가자는 kubelet의 요청만 따로 처리하고, 허용 범위가 좁습니다.
Kubelets are limited to reading their own Node objects, and only reading pods bound to their node.
읽기는 서비스·엔드포인트·노드·파드, 그리고 자기 노드에 바인딩된 파드와 관련된 시크릿·컨피그맵·PVC로 제한됩니다. 쓰기는 자기 노드 상태, 자기 파드 상태, 이벤트입니다.
"자기 것"을 판정하는 근거가 인증서의 CN에 박힌 노드 이름입니다. 그래서 CN이 틀리면 kubelet은 자기 노드 오브젝트조차 못 읽습니다.
여기에 어드미션 플러그인이 한 겹 더 있습니다.
The
NodeRestrictionadmission plugin restricts what kubelets can modify: Limits a kubelet to modify only its own node ... Limits a kubelet to modify only pods bound to itself.
인가가 "무엇을 할 수 있는가"를 정하고, NodeRestriction이 "그중에서도 자기 것만"을 강제합니다. 둘 다 켜야 노드 하나가 뚫렸을 때 다른 노드로 번지는 것을 막습니다.
정리하면 이렇게 이어집니다.
인증서의 O = system:nodes → Node 인가자가 이 요청을 처리 대상으로 인식
인증서의 CN = system:node:X → "자기 것"의 X 를 결정
NodeRestriction → X 이외의 것에 대한 쓰기를 거부
세 조각 중 하나만 어긋나도 kubelet이 동작하지 않거나, 반대로 지나치게 많은 권한을 갖게 됩니다.
# 6. API 서버 인증서에는 SAN이 중요하다
클라이언트 인증서는 CN/O가 핵심이지만, 서버 인증서는 SAN(Subject Alternative Name)이 핵심입니다. 클라이언트는 접속한 주소가 SAN 목록에 있는지 확인합니다.
API 서버에 접근하는 경로가 여럿이라 SAN도 여럿 필요합니다.
- 클러스터 내부 DNS 이름 (
kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local) - 서비스 ClusterIP (보통 서비스 대역의 첫 주소)
- 컨트롤 플레인 노드의 호스트명과 IP
- 로드 밸런서 주소가 있다면 그것도
나중에 접근 경로가 늘어나면 인증서를 다시 발급해야 합니다. 로드 밸런서를 앞에 붙이거나 새 DNS 이름을 쓰기 시작할 때 x509: certificate is valid for ..., not ... 오류로 나타납니다. 세울 때 넉넉히 넣어 두는 편이 낫습니다.
# 7. 인증서를 어디에 두는가
발급 후 배포 위치가 정해져 있습니다. 노드 인증서는 워커의 /var/lib/kubelet/으로, 서버 인증서는 컨트롤 플레인에 남습니다.
# 각 워커로 CA와 노드 인증서를 복사한다
for host in node-0 node-1; do
ssh root@$host mkdir -p /var/lib/kubelet/
scp ca.crt root@$host:/var/lib/kubelet/
scp $host.crt root@$host:/var/lib/kubelet/kubelet.crt
scp $host.key root@$host:/var/lib/kubelet/kubelet.key
done
파일 이름이 노드마다 달라지지 않고 목적지에서 통일된다는 점을 눈여겨볼 만합니다. 발급 시점에는 node-0.crt지만 노드 위에서는 kubelet.crt입니다. kubelet 설정이 고정 경로를 보기 때문이고, 그래서 설정 파일을 노드마다 다르게 만들 필요가 없습니다.
# 8. 실제 운영에서는 이렇게 하지 않는다
튜토리얼은 인증서를 손으로 만들지만, 이 방식은 노드가 늘어나면 유지되지 않습니다. 노드를 추가할 때마다 CA 키에 접근해 서명해야 하기 때문입니다.
실제 클러스터는 TLS 부트스트랩을 씁니다. kubelet이 부트스트랩 토큰으로 시작해 스스로 CSR을 제출하고, 컨트롤러가 승인하면 인증서를 받습니다. Node 인가자가 CSR API 접근을 허용하는 이유가 이것입니다.
read/write access to the CertificateSigningRequests API for TLS bootstrapping
이 방식의 이점이 둘입니다. CA 키가 노드로 나가지 않고, 인증서 만료 전에 kubelet이 스스로 갱신합니다. 손으로 발급한 인증서는 만료가 곧 장애이고, 만료일을 아무도 기억하지 않습니다.
# 지금 쓰는 인증서의 만료일 - 직접 발급했다면 달력에 적어 두어야 한다
openssl x509 -in /var/lib/kubelet/kubelet.crt -noout -dates -subject
튜토리얼에서 배울 것은 절차가 아니라 매핑입니다. CN이 사용자, O가 그룹, SAN이 서버 주소라는 사실은 관리형 클러스터에서도 그대로이고, 사용자 인증서를 발급하거나 접근 문제를 진단할 때 계속 쓰입니다.
# 9. 직접 확인하는 방법
만든 인증서가 의도한 신원인지 확인합니다.
# 주체와 SAN을 한 번에
openssl x509 -in node-0.crt -noout -subject -ext subjectAltName
# 출력 예: subject=CN = system:node:node-0, O = system:nodes
CA가 제대로 서명했는지도 봅니다.
openssl verify -CAfile ca.crt node-0.crt
클러스터가 실제로 나를 누구로 보는지가 최종 확인입니다.
# 서버가 인식한 사용자와 그룹
kubectl auth whoami
# 그 신원으로 무엇을 할 수 있는가
kubectl auth can-i --list
auth whoami의 결과가 인증서의 CN/O와 다르면 잘못된 kubeconfig를 쓰고 있는 것입니다. 인증서는 맞는데 권한이 없다면 인가 쪽 문제입니다.
# 10. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| kubelet이 401 | CA가 다르거나 인증서 손상 | openssl verify로 서명 확인 |
| kubelet이 403 | CN의 노드 이름 불일치 | 등록 노드 이름과 CN 대조 |
| 자기 노드도 못 읽음 | O가 system:nodes가 아님 | O 값 확인 |
| 다른 노드 것을 수정할 수 있음 | NodeRestriction 미활성 | 어드미션 플러그인 추가 |
x509: certificate is valid for ... | SAN에 접속 주소 없음 | 서버 인증서 재발급 |
| 스케줄러·컨트롤러가 권한 없음 | CN이 정해진 이름과 다름 | system:kube-* 이름 사용 |
| 어느 날 갑자기 전면 실패 | 인증서 만료 | 만료일 확인, 부트스트랩 전환 |
| 유출된 인증서를 못 막음 | 폐기 수단이 사실상 없음 | CA 교체 + 전체 재발급 |
# 11. 마무리
- 인증서를 발급하는 것이 곧 계정을 만드는 것입니다. 별도의 사용자 등록 API가 없는 이유입니다.
- CN은 사용자, O는 그룹이 됩니다. kubelet은
CN=system:node:<노드이름>,O=system:nodes여야 하고, 노드 이름이 글자 단위로 일치해야 합니다. - Node 인가는 CN에 박힌 이름으로 "자기 것"을 판정합니다.
NodeRestriction까지 켜야 노드 간 침범이 막힙니다. - 서버 인증서는 SAN이 핵심입니다. 접근 경로가 늘어나면 재발급이 필요하므로 처음에 넉넉히 넣습니다.
ca.key를 가진 사람은 아무 신원이나 발급할 수 있고, 인증서에는 실질적인 폐기 수단이 없습니다.- 실제 운영은 손 발급 대신 TLS 부트스트랩을 씁니다. CA 키가 노드로 나가지 않고 갱신이 자동입니다. 튜토리얼에서 가져갈 것은 절차가 아니라 필드와 권한의 매핑입니다.
# 참고
- Kubernetes The Hard Way - Provisioning a CA and Generating TLS Certificates (opens new window)
- Kubernetes - Using Node Authorization (opens new window)
- Kubernetes - Certificates and Certificate Signing Requests (opens new window)
- Kubernetes - TLS bootstrapping (opens new window)
- Kubernetes - PKI certificates and requirements (opens new window)
- Kubernetes - kube-controller-manager 플래그 레퍼런스 (opens new window)