TT Lab
시작하기
배우기 러닝패스 코스

CKA — 쿠버네티스 관리자

인증과 인가는 다른 층이다

TT Lab 에서 이어서 보기

한 줄 요약

apiserver 에 들어온 요청은 인증(너는 누구냐) → 인가(그 사람이 이걸 해도 되냐) → 어드미션(그 내용이 규칙에 맞냐) 순서로 통과합니다. RBAC 는 두 번째 층 하나이고, 첫 번째 층은 인증서와 토큰의 세계입니다. 이 둘을 섞으면 진단이 엉킵니다.

왜 이게 필요했나

쿠버네티스에는 사용자 오브젝트가 없습니다. kubectl get users 는 없습니다. 왜냐면 apiserver 는 신원 발급자가 아니라 검증자이기 때문입니다. 신원의 출처는 밖에 있습니다.

그래서 "그 사용자를 지워 주세요" 라는 요청은 성립하지 않습니다. 인증서를 폐기하거나 바인딩을 지우는 것이 전부입니다.

인가 쪽은 네 종류의 오브젝트뿐입니다.

네임스페이스 범위 클러스터 범위
권한 정의 Role ClusterRole
권한 부여 RoleBinding ClusterRoleBinding

여기서 딱 하나 헷갈리는 조합이 있습니다. RoleBinding 이 ClusterRole 을 참조하는 것은 허용되며, 그 경우 ClusterRole 의 규칙이 그 RoleBinding 이 있는 네임스페이스 안에서만 적용됩니다. view, edit 같은 내장 ClusterRole 을 네임스페이스별로 재사용하는 표준 방법입니다. 반대로 ClusterRoleBinding 을 쓰면 전 클러스터로 퍼지므로, 네임스페이스 경계를 지키려다 여기서 실수하는 일이 많습니다.

RBAC 는 누적만 됩니다. 거부 규칙이 없습니다. 어떤 바인딩이든 한 번 허용하면 허용입니다. 그래서 "왜 이 계정이 이걸 할 수 있지" 를 추적할 때는 모든 바인딩을 훑어야 하고, kubectl auth can-i --as= 가 그 지름길입니다. 이 명령은 SubjectAccessReview 를 만들어 apiserver 에 물어볼 뿐 실제 요청을 보내지 않습니다. 부작용 없이 안전하게 확인할 수 있습니다.

요청이 apiserver 안에서 통과하는 세 관문

kubectl 이든 파드 안의 클라이언트든 apiserver 에 들어온 요청은 같은 순서로 지난다. RBAC 는 그중 가운데 한 층일 뿐이다.

  1. 인증: 너는 누구인가클라이언트 인증서의 CN 은 사용자 이름, O 는 그룹이 된다. 파드 안 워크로드의 신원은 ServiceAccount 토큰이다. 사용자 오브젝트는 없고 apiserver 는 신원을 발급하지 않고 검증만 한다.
  2. 인가: 그 신원이 이걸 해도 되는가RBAC 가 일하는 층이다. Role, ClusterRole 과 두 종류의 바인딩이 전부이고 거부 규칙은 없다. 어떤 바인딩이든 허용하면 허용이다.
  3. 어드미션: 그 내용이 규칙에 맞는가인가를 통과한 요청의 내용을 검증과 어드미션 웹훅이 본다. 허용된 신원이어도 이 층에서 거부될 수 있다.

여기서 구분할 것 RBAC 가 허용했다는 것은 두 번째 관문을 지났다는 뜻일 뿐이다. 인증서와 토큰의 문제는 첫째 층, 규칙과 웹훅의 문제는 셋째 층이라 RBAC 를 고쳐도 풀리지 않는다.

잠깐, 예측해 보세요 create pods 를 허용하는 RoleBinding 이 이미 있는데 파드 생성이 어드미션 웹훅 때문에 거부됐다. RBAC 규칙을 더 넓혀야 할까?

설명 확인 · 채점 없는 자가 점검

아니다. 인가는 이미 통과했고 거부는 셋째 관문에서 났다. 권한을 넓혀도 이 거부는 풀리지 않으니 웹훅이 무엇을 요구하는지를 봐야 한다.

근거 문서

어떻게 동작하나

Aggregated ClusterRole 은 규칙을 직접 쓰지 않는 ClusterRole 입니다. aggregationRule 에 라벨 셀렉터를 적으면 컨트롤러가 그 라벨이 붙은 다른 ClusterRole 들을 찾아 rules 를 채워 넣습니다. CRD 로 새 리소스를 추가했을 때 기존 view/edit 역할에 라벨 하나로 얹을 수 있는 확장점입니다. 여기에 손으로 rules 를 쓰면 컨트롤러가 덮어씁니다.

automountServiceAccountToken 은 파드에 API 토큰을 넣을지 결정합니다. 기본은 넣습니다. 그러면 API 를 전혀 쓰지 않는 nginx 파드에도 토큰이 들어가고, 컨테이너가 털리면 그 토큰이 곧 클러스터 접근권입니다. ServiceAccount 나 파드 스펙에서 false 로 두면 kube-api-access-* 투영 볼륨 자체가 주입되지 않습니다.

현장에서 만나는 모습

사례 1 — CA 개인키의 노출 창을 2시간으로 자른다. 홈랩을 3노드에서 7노드로 늘릴 때, 워커 조인과 컨트롤 플레인 조인의 차이가 인증 구조를 그대로 보여 줬습니다. 워커는 토큰 하나면 끝납니다.

kubeadm token create --print-join-command

컨트롤 플레인은 다릅니다. 새 노드도 인증서를 발급하는 주체가 되어야 하므로 CA 개인키가 필요합니다. kubeadm 은 이 민감한 파일을 scp 로 나르게 하지 않고, kubeadm init phase upload-certs --upload-certs 로 CA 키 묶음을 암호화해 클러스터 안의 Secret 으로 올립니다. 조인 명령에 주는 64자리 certificate-key 가 그 암호문을 푸는 대칭키입니다.

그리고 이 Secret 은 2시간 뒤 자동 삭제됩니다. 암호화됐다고는 해도 클러스터의 루트 신뢰가 안에 들어 있으니 노출 창을 최소화하려는 설계입니다. 만료되면 upload-certs 를 다시 실행하면 되고 기존 클러스터에는 영향이 없습니다. 인증의 핵심은 이렇게 "신뢰의 뿌리를 얼마나 짧게 노출하느냐" 입니다.

사례 2 — 인증서 SAN 이 곧 접근 권한이다. 같은 클러스터에서 컨트롤 플레인 노드를 셋으로 늘렸는데도 첫 노드가 죽으면 아무도 붙지 못했습니다. apiserver 인증서의 SAN 에 나머지 두 노드의 IP 가 없었기 때문입니다. RBAC 를 아무리 잘 짜도 TLS 검증을 통과하지 못하면 인가 층에 도달조차 못 합니다. 반대로 이 성질을 역이용해, 죽은 노드의 IP 를 살아 있는 노드의 인터페이스에 얹으면 인증서 재발급도 kubeconfig 수정도 없이 모든 클라이언트가 그대로 재접속합니다. 클라이언트가 검증하는 것은 노드가 아니라 접속한 주소가 SAN 에 있는가 이기 때문입니다.

사례 3 — 권한이 아니라 설정이 문제일 때도 있다. GPU Operator 를 올리다 파드가 전부 Init 에서 멈춘 적이 있습니다. kubectl get runtimeclass 에는 nvidia 가 멀쩡히 있었지만 노드의 containerd 설정에는 nvidia 가 한 글자도 없었습니다. 이름표는 쿠버네티스 오브젝트로 존재하고 실체는 노드에 있어야 하는데, 후자가 없었던 것입니다. 오브젝트가 있다는 것과 실제로 동작한다는 것은 다른 명제 — 인가 문제를 진단할 때도 같은 습관이 필요합니다. can-i 가 yes 를 뱉었다면 그 다음은 인증서, 네트워크, 어드미션 웹훅 순으로 내려가야 합니다.

인가에 닿기 전에 끝나는 요청

본문의 사례다. 컨트롤 플레인을 세 대로 늘렸는데 첫 노드가 죽자 아무도 붙지 못했다. 나머지 두 노드의 IP 가 apiserver 인증서의 SAN 에 없었기 때문이다.

  • 접속한 주소가 SAN 에 있다TLS 검증을 통과해 인증과 인가까지 요청이 도달한다. RBAC 가 의미를 갖는 것은 이 경우부터다.
  • 접속한 주소가 SAN 에 없다TLS 검증에서 끝나 인가 층에 도달하지 못한다. RBAC 를 아무리 잘 짜도 결과가 같다.

여기서 구분할 것 클라이언트가 검증하는 것은 어느 노드냐가 아니라 접속한 주소가 SAN 에 있느냐다. 그래서 죽은 노드의 IP 를 살아 있는 노드에 얹으면 인증서와 kubeconfig 를 그대로 둔 채 재접속이 된다.

잠깐, 예측해 보세요 cp-2 의 IP 로 직접 붙었더니 x509 인증서 오류가 난다. 그 주소로 접속하는 사용자에게 Role 을 더 줘야 할까?

설명 확인 · 채점 없는 자가 점검

아니다. 요청이 TLS 검증에서 끝나 인가 층까지 가지 못했다. 접속한 주소가 apiserver 인증서의 SAN 에 들어 있는지부터 본다.

근거 문서

다음 실습에서 할 것

ServiceAccount 를 만들고 Role/RoleBinding 으로 읽기 권한만 주고, kubectl auth can-i --as= 로 된다는 것과 안 된다는 것을 둘 다 확인합니다. 그 다음 ClusterRole/ClusterRoleBinding 으로 클러스터 스코프 리소스를 열고, Aggregated ClusterRole 을 구성하고, 마지막으로 토큰이 아예 주입되지 않는 파드를 만듭니다.