Chapter 12

쿠버네티스 API 서버 보안

  • 12.1 인증 — 누가 API 서버에 접근하는가
  • 12.2 RBAC으로 클러스터 보안하기
  • 12.3 기본 ClusterRole과 권한 설계

11장에서 "쿠버네티스 내부가 어떻게 돌아가는가"를 봤다면, 12장은 그 API 서버를 어떻게 잠가야 하는가의 이야기야. 지금까지 대충 default ServiceAccount로 아무거나 다 해왔던 게 사실 위험했다는 걸 깨닫는 장이지. 핵심은 두 단계 — **Authentication(누구인가)**과 Authorization(뭘 할 수 있는가). 후자가 바로 RBAC이야.

먼저 인증부터. API 서버는 모든 요청에 대해 인증 플러그인들을 순차적으로 돌려. 플러그인은 요청자의 usernamegroup 목록을 뽑아내고, 쿠버네티스는 그걸 그대로 Authorization 단계로 넘겨. 쿠버네티스 자체는 사용자 정보를 저장하지 않는다는 게 포인트야 — users는 외부 시스템(SSO 같은 거)에서 관리해. 클라이언트는 두 종류로 나뉘어. **Users(사람)**는 외부에서 관리되고 쿠버네티스 리소스로 존재하지 않아. 만들거나 지울 수 없어. **ServiceAccounts(파드)**는 클러스터 내부 리소스고 네임스페이스에 속해. 파드 안의 앱이 API 서버와 대화할 때 쓰는 신원이야. 두 종류 모두 group에 속할 수 있고, built-in 그룹들이 있어 — system:unauthenticated(인증 실패한 요청), system:authenticated(인증 성공한 모든 사용자), system:serviceaccounts(모든 SA), system:serviceaccounts:<namespace>(특정 ns의 모든 SA).

ServiceAccount의 정체를 좀 더 보면, 모든 파드는 정확히 하나의 ServiceAccount에 연결돼. 지정 안 하면 네임스페이스의 default SA를 써. SA의 토큰은 파드 컨테이너 안에 Secret volume으로 자동 마운트되는데, 위치는 /var/run/secrets/kubernetes.io/serviceaccount/token으로 고정이야. 이 토큰은 JWT고, 앱이 API 서버에 이 토큰을 실어 보내면 인증 플러그인이 system:serviceaccount:<namespace>:<sa-name> 형식의 username으로 매핑해줘. 파드는 자기 네임스페이스의 SA만 쓸 수 있어. cross-namespace SA 사용은 불가능해. SA를 만들려면 kubectl create serviceaccount foo 한 줄이면 SA와 연관 토큰 Secret까지 자동 생성되고, 파드에 붙일 땐 spec.serviceAccountName 필드를 써. 한 번 지정하면 변경 불가야 — 파드 만들 때만 설정할 수 있어. SA에는 두 가지 부가 기능이 있는데, Mountable secretskubernetes.io/enforce-mountable-secrets="true" 어노테이션을 달면 그 SA를 쓰는 파드가 SA에 명시된 Secret만 마운트할 수 있게 임의 Secret 접근을 차단해주고, Image pull secrets는 SA에 적어두면 그 SA를 쓰는 모든 파드에 자동으로 imagePullSecret이 붙어서 파드마다 적을 필요가 없어.

이제 RBAC. RBAC은 1.6부터 들어왔고 1.8에서 GA가 됐어. RBAC 이전엔 SA 토큰만 뺏으면 클러스터에서 뭐든 할 수 있었거든 — path traversal로 토큰 탈취하는 데모가 실제로 돌아다녔을 정도야. 지금은 기본 SA는 아무것도 못 봐. 뭔가 하려면 권한을 명시적으로 부여해야 해. REST 요청(GET/POST/PUT 등)은 RBAC 동사로 매핑돼. GET·HEAD는 단일 리소스에선 get(watch 포함), 컬렉션에선 list(watch 포함). POST는 create, PUT는 update, PATCH는 patch, DELETE는 delete(컬렉션이면 deletecollection). RBAC은 "이 subject가 이 verb를 이 resource에 대해 할 수 있는가"를 평가해. subject는 사람, SA, 또는 그룹이고, 여러 Role이 있으면 합집합이라 하나라도 허용하면 통과해.

RBAC은 딱 네 가지 리소스로 구성돼. Role / ClusterRole은 "뭘 할 수 있나"를 정의하고(verb + resource), RoleBinding / ClusterRoleBinding은 "누가 그 Role을 쓰나"를 정의해(subject에 연결). Role은 namespaced, ClusterRole은 cluster-level. 같은 법칙이 Binding에도 적용돼. 가장 흔한 패턴인 Role + RoleBinding을 보자. foo 네임스페이스의 SA가 Service를 조회하게 하려면, Role 매니페스트에 apiGroups: [""](core API group은 빈 문자열), verbs: ["get", "list"], resources: ["services"](반드시 복수형!)을 적어. 그리고 kubectl create rolebinding test --role=service-reader --serviceaccount=foo:default -n foo로 묶어. RoleBinding은 다른 네임스페이스의 SA도 subject로 가질 수 있어. 즉 foo 네임스페이스의 Role을 bar 네임스페이스의 SA에게 줘서 bar의 파드가 foo의 Service를 읽게 만들 수 있어.

ClusterRole + ClusterRoleBinding이 필요한 상황은 세 가지야. cluster-level 리소스(Node, PV, Namespace 같은 거) 접근, non-resource URL(/healthz, /api, /version) 접근, 같은 Role을 여러 네임스페이스에서 재사용하고 싶을 때. 함정이 있는데, PV 같은 cluster-level 리소스에 접근하려면 반드시 ClusterRoleBinding을 써야 해. RoleBinding으로 ClusterRole을 참조하는 건 namespaced 리소스에만 동작해. PV에 대해선 RoleBinding이 아무 효과가 없어. 그리고 흥미로운 조합이 ClusterRole + RoleBinding이야. view 같은 기본 ClusterRole은 Pod, Service, ConfigMap 등 namespaced 리소스 규칙을 담고 있는데, 이걸 어떻게 바인딩하느냐에 따라 범위가 달라져. ClusterRoleBinding으로 묶으면 모든 네임스페이스에서 읽기 가능, 특정 ns의 RoleBinding으로 묶으면 그 ns에서만 읽기 가능. 즉 같은 ClusterRole을 각 네임스페이스마다 RoleBinding으로 재사용할 수 있다는 뜻이야. Role을 네임스페이스마다 복붙할 필요가 없어져.

쿠버네티스는 API 서버가 시작될 때마다 기본 ClusterRole/ClusterRoleBinding을 (재)생성해. 실수로 지워도 다시 만들어진다는 뜻이야. 가장 중요한 네 개를 보면, view는 대부분의 리소스 읽기인데 단, Role, RoleBinding, Secret은 제외돼. Secret을 제외하는 이유는 privilege escalation 방지야 — Secret 안에 더 강한 SA 토큰이 들어있을 수 있거든. edit은 리소스 수정 + Secret 읽기·쓰기인데, Role/RoleBinding은 건드릴 수 없어 (역시 escalation 방지). admin은 네임스페이스 내 전권이고 ResourceQuota와 Namespace 자체만 제외돼. Role/RoleBinding도 관리 가능해. cluster-admin은 클러스터 전체 전권이야. 이걸 ClusterRoleBinding으로 주면 신이 돼. system: 접두사가 붙은 것들은 쿠버네티스 컴포넌트용이야 — system:kube-scheduler, system:node, system:kube-controller-manager 같은 거.

privilege escalation 방어 메커니즘이 하나 더 있어. API 서버는 자기가 이미 가진 권한을 넘어서는 Role은 만들 수 없게 막아. 즉 edit 권한만 가진 사람이 cluster-admin Role을 만들어서 자기에게 바인딩하는 식의 공격은 차단돼. 마지막으로 권한 설계 원칙 몇 가지. default SA에 아무 권한도 주지 마. 기본 SA는 non-resource URL 말곤 진짜 아무것도 못 봐. 그 상태를 유지해. 파드마다 전용 SA를 만들어. 읽기만 필요한 파드랑 수정이 필요한 파드는 다른 SA를 써야 해. RoleBinding을 우선으로 — ClusterRoleBinding은 꼭 필요할 때만. 파드가 다른 네임스페이스를 건드릴 일은 거의 없어. 그리고 앱은 언젠가 뚫린다고 가정해. 토큰이 유출됐을 때 피해를 최소화하는 게 목표야.


정리

12장 읽고 기억할 거 세 가지:

  1. 인증과 인가는 분리된 두 단계다. 인증 플러그인은 username과 group만 뽑아낸다. 실제로 뭘 할 수 있는지는 RBAC이 결정한다. 파드의 신원은 ServiceAccount이고, 이건 네임스페이스 단위로 관리되는 쿠버네티스 리소스다.
  2. Role은 "뭘", Binding은 "누가". Role/ClusterRole이 verb+resource 권한 묶음을 정의하고, RoleBinding/ClusterRoleBinding이 그걸 subject(SA, user, group)에 연결한다. PV 같은 cluster-level 리소스는 반드시 ClusterRoleBinding을 써야 한다 — RoleBinding으론 안 먹는다.
  3. 기본 SA는 무권한으로 유지하고, 파드별로 최소 권한 SA를 만들어라. view/edit/admin/cluster-admin 기본 ClusterRole이 있지만, 파드에 cluster-admin을 주는 건 재앙. edit이 Secret을 읽을 수 있어도 Role/RoleBinding은 못 건드리는 이유는 privilege escalation 방지다 — 이 패턴을 직접 설계할 때도 명심해야 한다.