RBAC — Four Objects and One Trap
한국어 원문으로 표시합니다.
한 줄 요약
RBAC 는 "무엇을 할 수 있는가"(Role)와 "누가 어디서 그것을 할 수 있는가"(Binding)를 분리한 모델이고, 범위를 정하는 쪽은 언제나 Binding 이다.
왜 이게 필요했나
권한을 파일 하나에 다 적으면 두 가지가 곧바로 무너진다. 사람이 늘어날 때마다 같은 권한 묶음을 복사해야 하고, 어떤 권한이 어디까지 미치는지 아무도 추적하지 못하게 된다. RBAC 는 이 둘을 갈라 놓는다. 권한의 내용은 Role 과 ClusterRole 이 정의하고, 그 내용을 누구에게 어느 범위로 줄지는 RoleBinding 과 ClusterRoleBinding 이 정한다.
| 오브젝트 | 스코프 | 하는 일 |
|---|---|---|
| Role | 네임스페이스 | 그 네임스페이스 리소스에 대한 규칙 |
| ClusterRole | 클러스터 | 클러스터 범위 리소스 또는 재사용할 공통 권한 |
| RoleBinding | 네임스페이스 | 역할을 주체에게, 이 네임스페이스에서만 |
| ClusterRoleBinding | 클러스터 | 역할을 주체에게, 클러스터 전체에서 |
어떻게 동작하나
규칙 하나는 세 조각이다. apiGroups, resources, verbs.
rules:
- apiGroups: [""] # 코어 그룹은 빈 문자열
resources: ["pods"]
verbs: ["get", "list", "watch"]
apiGroups: [""] 를 빠뜨려 파드가 안 보인다는 문의는 지금도 매주 나온다. 코어 API 그룹의 이름은 빈 문자열이고, Deployment 같은 것은 apps 그룹에 있다.
여기서 가장 중요한 함정이 나온다. ClusterRole 을 RoleBinding 으로 걸면 그 권한은 해당 네임스페이스에서만 유효하다. 즉 역할의 종류가 스코프를 정하는 것이 아니라 바인딩의 종류가 정한다. 이 조합은 실무에서 대단히 유용하다. "읽기 전용" 같은 공통 권한 묶음을 ClusterRole 로 한 번만 정의해 두고, 팀마다 자기 네임스페이스에서 RoleBinding 으로 걸어 쓰면 된다. 같은 ClusterRole 을 ClusterRoleBinding 으로 걸었다면 그 순간 클러스터 전체 권한이 된다. 두 매니페스트의 차이는 단어 하나다.
두 번째로 자주 놓치는 것이 서브리소스다. pods/log 와 pods/exec 는 pods 와 별개의 리소스 이름이다. 파드를 읽을 권한이 있다고 로그가 읽히지 않고, 로그를 읽을 권한이 있다고 셸에 붙을 수 있는 것도 아니다. 로그 수집 봇에게 pods/log 의 get 만 주는 설계가 가능한 이유이고, 반대로 pods/exec 를 아무에게나 열어 두면 컨테이너 안에서 무엇이든 할 수 있게 된다는 뜻이기도 하다.
세 번째는 확인 방법이다. 권한은 작성한 것이 아니라 판정되는 것이므로 눈으로 읽지 말고 물어봐야 한다.
kubectl auth can-i list pods -n rbac-lab \
--as=system:serviceaccount:rbac-lab:app-reader
# yes
--as 는 impersonation 이다. 서비스 어카운트의 정식 이름 형식이 system:serviceaccount:<네임스페이스>:<이름> 이라는 것만 알면 어떤 주체로든 흉내 내어 물어볼 수 있다. 되는 것과 안 되는 것을 함께 확인해야 의미가 있다. yes 만 확인하면 권한이 필요 이상으로 넓어도 알 수 없다.
현장에서 만나는 모습
첫째, 와일드카드는 한 번 들어가면 안 빠진다. verbs: ["*"] 는 편하지만, 나중에 새 리소스가 생기면 아무도 모르는 사이에 권한이 자동으로 넓어진다. 최소 권한은 취향이 아니라 시간이 지나도 범위가 커지지 않게 만드는 장치다.
둘째, get secrets 는 사실상 자격증명 열람 권한이다. 개발자에게 편의로 준 편집 권한이 프로덕션 데이터베이스 비밀번호 열람 권한과 같아지는 경우가 흔하다. 이 이야기는 다음 모듈로 이어진다.
셋째, 정기 감사. cluster-admin 이 걸린 ClusterRoleBinding 목록, 와일드카드를 쓴 역할, secrets 를 읽을 수 있는 역할 — 이 세 가지 질문은 클러스터마다 주기적으로 던져야 한다. kubectl get clusterrolebindings -o json | jq 몇 줄이면 답이 나오고, 이 습관 하나가 권한 부채가 쌓이는 속도를 크게 낮춘다.
권한이 왜 안 되는지 알아내는 법
RBAC 문제는 추측하지 않고 물어볼 수 있다. kubectl auth can-i 가 API 서버에
직접 판정을 묻는다.
kubectl auth can-i create pods -n prod
kubectl auth can-i list secrets -n prod --as=system:serviceaccount:prod:app
kubectl auth can-i --list --as=system:serviceaccount:prod:app -n prod
마지막 줄이 특히 유용하다. 그 주체가 가진 권한 전부를 표로 보여 준다.
대상을 정확히 적어야 한다. 서비스 계정의 이름은
system:serviceaccount:<네임스페이스>:<이름> 이다. 사람 계정은 인증서의 CN 이나
OIDC 의 클레임에서 온다. --as 로 흉내 내려면 그 자체가 impersonate 권한을
요구한다.
규칙은 더해지기만 한다. RBAC 에는 거부가 없다. 권한이 너무 넓다면 어딘가에서 더해 주고 있는 것이므로, 바인딩을 찾아야 한다.
kubectl get rolebinding,clusterrolebinding -A -o json | jq -r '
.items[] | select(.subjects[]? .name=="app")
| "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name) -> \(.roleRef.name)"'
흔히 헷갈리는 네 가지.
ClusterRole을RoleBinding으로 묶으면 그 네임스페이스 안에서만 유효하다. 같은 역할을 여러 네임스페이스에 재사용하는 정상적인 방법이다.- 노드·PV·네임스페이스처럼 네임스페이스가 없는 자원은
ClusterRoleBinding이 필요하다. - 하위 자원은 따로 적는다.
pods/log,pods/exec,deployments/scale은pods,deployments에 포함되지 않는다. create권한만으로 특정 이름을 막을 수 없다.resourceNames는 이름을 알아야 하는 동사(get·update·delete)에서만 동작한다.
기본으로 붙는 것을 잊지 않는다. 모든 인증된 사용자는
system:discovery 등으로 API 목록을 볼 수 있고, 파드에 마운트된 서비스 계정
토큰은 아무 권한이 없어도 API 서버에 닿을 수 있다. 필요 없으면
automountServiceAccountToken: false 로 끈다.
다음 실습에서 할 것
읽기 전용 Role 을 만들어 서비스 어카운트에 바인딩하고 auth can-i 로 경계를 확인한다. 노드 같은 클러스터 범위 리소스를 위한 ClusterRole 을 만들고, 같은 ClusterRole 을 RoleBinding 으로 걸어 스코프가 어떻게 좁아지는지 직접 본다. 서브리소스만 여는 역할을 만들고, 마지막에는 클러스터의 위험한 권한을 훑는 감사 보고서를 JSON 으로 만든다.