0. Contents
1. Overview
24장과의 관계
- 24장: 파드가 밖으로 뻗는 손을 자르는 이야기
- 대상은 파드에서 파드로 가는 HTTP
- 26장: 그 손이 쿠버네티스 자신을 조작하려 할 때 무엇을 허락할지 정하는 이야기
- 대상은 파드에서 API 서버로 가는 요청
이 장의 축: 신분 관리의 비대칭
- 쿠버네티스는 자기 신분 관리를 하지 않기로 결정했다
kubectl create user같은 명령은 없다
- 사람 계정은 회사 시스템에 맡긴다
- 쿠버네티스는 인증 결과로 넘어온 문자열 몇 개만 보고 권한을 판단한다
- 반대로 파드는 자기가 만들었으니 신분증도 자기가 발급한다
- 이 비대칭이 이 장의 거의 모든 구조를 설명한다
2. Problem
2.1 100만 개의 열린 문 — 잠그지 않아서 뚫린다
2022년의 노출 규모
- 보안 연구자들이 인터넷에 노출된 쿠버네티스 인스턴스를 약 100만 개 찾아냈다
- 특별한 기술이 필요하지도 않았다 — 스캐너를 돌렸더니 그냥 열려 있었다
원인: 아무도 잠그지 않은 문
- 해킹당한 게 아니다
- 취약점을 뚫은 게 아니라, 안내 데스크에 아무도 앉히지 않은 건물로 걸어 들어간 것에 가깝다
- 24장의 flat network가 “기본값이 다 열림"이었듯, 여기서도 원인은 같다
- 잠그는 일을 아무도 안 했다
무대 비유: 회사 건물
- 이 장의 무대를 건물로 그려두면 뒤가 편하다
| 쿠버네티스 | 건물 |
|---|---|
| 클러스터 | 회사 건물 |
| API 서버 | 정문 안내 데스크 — 창문도 뒷문도 없다 |
| 컨트롤 플레인 | 관제실 — 앉으면 모든 층의 잠금장치를 조작할 수 있다 |
| 노드 / 파드 | 각 사무실과 그 안의 직원들 |
네임스페이스와 RBAC 경계
- 24장 2.1의 “네임스페이스는 폴더다"와 짝이 되는 문장
- 네임스페이스는 RBAC의 경계이긴 하지만, 그 경계는 규칙을 적어야 생긴다
- 아무 Role도 만들지 않으면 경계도 없다
2.2 인증과 인가 — 누구인가와 무엇을 할 수 있는가
보안의 두 단어
- 보안의 핵심에 두 단어가 있고, 이 장은 둘을 명확히 나눠 쓴다
| Authentication (인증) | Authorization (인가) | |
|---|---|---|
| 질문 | 너 누구야? | 그거 해도 돼? |
| 다루는 것 | 주체(subject)의 신원 | 리소스에 대해 허용되는 동작 |
| 누구의 일 | 관리자 — 회사 계정 시스템 연동 | 개발자 — 배포할 때마다 YAML |
| 실패하면 | 401 Unauthorized | 403 Forbidden |
| 이 책의 분량 | 훑고 지나감 | 장의 대부분 |
책이 인증을 짧게 다루는 이유
- 인증 설정은 API 서버를 띄울 때 주는 실행 옵션(
--oidc-issuer-url같은)이다- 개발자가 나중에 바꿀 수 있는 게 아니다
- 반면 인가는 Role과 RoleBinding이라는 평범한 YAML이다
- Git에 커밋하고 앱과 함께 배포한다
- 24장에서 NetworkPolicy가 “앱 옆에 두는 방화벽"이었던 것과 정확히 같은 자리다
401과 403의 구분
- 실전에서 바로 쓰인다
- 401이면 토큰·인증서 문제, 403이면 RBAC 문제다
- 어느 관문에서 걸렸는지가 에러 코드에 이미 적혀 있다
2.3 오퍼레이터의 딜레마 — 구조상 큰 권한이 필요하다
권한을 잘못 주면 양쪽으로 터진다
| 권한 | 결과 | 모습 |
|---|---|---|
| 너무 많으면 | 권한 상승(privilege escalation) | 앱 하나 뚫렸는데 옆 팀 Secret 읽고 Deployment 수정 → 클러스터 전체 |
| 너무 적으면 | 배포 실패(deployment failure) | apply 는 성공했는데 실행 중 “Forbidden” 으로 죽는다 |
흔한 실수: 배포 실패를 없애려다 권한 상승을 만든다
- 아래 문제를 빨리 없애려다 위 문제를 만든다
- Forbidden이 자꾸 뜨니 귀찮아서 권한을 넓게 열어버리는 것이다
- 두 문제는 사실 이어져 있다
딜레마가 가장 날카로운 곳: 오퍼레이터(28장)
- CRD로 쿠버네티스 API를 확장하는 컨트롤러·오퍼레이터가 하는 일
- 클러스터 전역을 계속 watch — “어디든 내 CRD 가 생기면 알려줘”
- 변화를 감지하면 Pod, Service, Secret 을 직접 만들고 지운다
- 이걸 한 네임스페이스가 아니라 여러 곳에서 한다
- 즉 오퍼레이터는 태생적으로 넓은 권한을 요구한다
- 설정 실수가 아니라 패턴 자체의 성질이다
- Prometheus Operator, cert-manager처럼 남이 만든 것을 가져다 쓰는 경우가 대부분이다
- 코드를 다 읽어보지 않은 프로그램에 클러스터 조작 권한을 주는 셈이 된다
책의 답: 영향 제한
- “권한을 주지 마라"가 아니다 — 그러면 일을 못 한다
- 잘게 쪼개서 최소한만 주고, 뚫렸을 때 피해 범위를 좁혀두라는 것이다
- 원문의 표현은 “limit the impact of any potential security breaches”
- 침해를 막는 게 아니라 영향을 제한한다
- 24장의 blast radius 축소와 같은 사고방식이다
이 장의 목표
- 모든 요청이 지나는 단 하나의 문에서, 개발자가 앱과 함께 배포하는 권한 규칙으로, 뚫렸을 때 닿을 수 있는 범위를 미리 좁히는 것
3. 세 관문 — 모든 요청이 지나는 길
3.1 API 서버는 문이 하나다
모든 요청이 지나는 세 단계
- 쿠버네티스 API 서버로 들어오는 모든 요청은 세 단계를 순서대로 지난다
그림의 요점: 바깥 테두리
- 정작 중요한 건 세 상자가 아니라 바깥 테두리다
- 세 관문이 전부 API 서버 안에 있고, 우회로가 없다
kubectl명령이든, 파드 안 오퍼레이터의 API 호출이든, 스케줄러가 보내는 내부 요청이든 예외가 없다
순서대로 통과
- 이것도 중요하다
- 앞에서 막히면 뒤는 실행조차 되지 않는다
| 실패한 관문 | 응답 | 뜻 |
|---|---|---|
| 1관문 | 401 Unauthorized | “너 누군지 모르겠다” |
| 2관문 | 403 Forbidden | “누군지는 알겠는데 그건 안 된다” |
| 3관문 | 403 + admission webhook 메시지 | — |
헷갈리는 지점: 3관문의 403
- 세 번째 줄이 실전에서 헷갈리는 지점이다
- 403이 떴다고 무조건 RBAC를 뒤지면 안 된다
- 에러 메시지에
admission webhook "..." denied the request가 있으면- 권한은 있는데 내용이 규정에 안 맞아서 막힌 것이다
3.2 인증 — 다섯 개의 입구, 하나의 출구
가장 먼저 새길 사실
- 쿠버네티스에는 사용자 계정 시스템이 없다
kubectl create user도,kubectl get users도 없다- 대신 콘센트처럼 만들어져 있다
- 회사가 이미 쓰는 인증 시스템을 규격에 맞게 꽂는다
계정 시스템을 두지 않은 이유
- 자체 계정 시스템을 만들면 회사가 직원 계정을 두 벌 관리해야 한다
- 퇴사자를 한쪽에서만 지우는 순간 그게 곧 구멍이 된다
꽂을 수 있는 다섯 가지
| 방식 | 어떻게 | 비유 | 특징 |
|---|---|---|---|
| OIDC 베어러 토큰 | 외부 제공자가 발급한 JWT를 Authorization 헤더에 | “구글로 로그인” | 사람 사용자의 표준. 퇴사 시 자동 차단 |
| X.509 인증서 | TLS 인증서 제시. CN→username, O→group | 여권 | 내부 컴포넌트용. 폐기가 사실상 안 된다 |
| 인증 프록시 | 앞에 세운 프록시가 확인 후 헤더로 전달 | 경비 초소 | 우회당하면 치명적 |
| 정적 토큰 파일 | CSV 파일에서 토큰 대조 | 출석부 | 만료 없음, 수정 시 재시작. 실무 부적합 |
| 웹훅 | 외부 서버에 물어봄 | 본사에 전화 | 표준으로 안 되는 체계용. 매 요청 네트워크 호출 |
이 절의 가장 큰 함정: 하나만 성공하면 통과
- 여러 개를 동시에 켤 수 있고, 하나만 성공하면 통과한다
- 인증 방식을 하나 더 켜는 것
- = 보안 강화 ❌
- = 입구를 하나 더 여는 것 ✅
- OIDC를 아무리 엄격하게 설정해도, 예전에 테스트하려고 켜둔 정적 토큰 파일이 남아 있으면 공격자는 그쪽으로 들어온다
- 보안은 가장 약한 고리로 결정된다
- 안 쓰는 인증 플러그인은 반드시 꺼야 한다
평가 순서가 고정돼 있지 않다
- kubeconfig에 인증서와 토큰이 둘 다 있으면 어느 쪽으로 인증될지 모른다
- 방식마다 username이 다를 수 있다(
hongvshong@company.com)- RBAC는 그 문자열로 판단하니 권한이 달라진다
- 코드도 설정도 안 바꿨는데 어제 되던 게 오늘 403이 나는, 원인 찾기 지독한 버그다
- 해법은 단순하다 — 클라이언트 하나에 자격 증명 하나만
3.3 사용자 표현 — 인증이 남기는 것
결과물은 하나의 형태
- 방식은 다섯 개인데 결과물은 하나의 형태로 통일된다
- 추출 방법은 전부 다르지만(JWT 파싱, 인증서 필드 읽기, 헤더 읽기, CSV 조회, 외부 호출) 나오는 것은 같다
| 필드 | RBAC가 쓰나 | 용도 |
|---|---|---|
| username | ✅ | 권한 판단, 감사 로그 |
| UID | ❌ | 이름 재사용 구분 — 퇴사한 alice와 새로 온 alice |
| groups | ✅ | 권한 판단 |
| extra | ❌ | 인가 웹훅용 부가 정보 |
RBAC가 보는 것은 둘뿐
- RBAC가 실제로 보는 건 두 개뿐이다
- 인증이 끝나는 순간 토큰은 사라진다
- 뒷단계는 어떤 방식으로 인증됐는지 모르고, 알 필요도 없다
설계 효과: 서로의 내부를 모르는 구조
- 인증 방식을 추가해도 RBAC를 손댈 필요가 없다
- 반대로 인가를 웹훅으로 바꿔도 인증은 그대로다
- 24장 5.2에서 본 컨트롤 플레인과 데이터 플레인의 분리처럼, 정해진 규격으로만 소통하고 서로의 내부는 모른다
쿠버네티스가 자동으로 붙이는 그룹
- groups에
system:authenticated가 섞여 있는 게 눈에 띈다 - 어느 계정 시스템에도 없는, 쿠버네티스가 자동으로 붙이는 그룹이다
| 그룹 | 언제 붙나 |
|---|---|
system:unauthenticated | 자격 증명 없이 온 요청 — 여기 권한 주면 익명 접근이 열린다 |
system:authenticated | 어떤 방식으로든 인증 성공한 모든 요청 |
system:masters | RBAC를 아예 건너뛴다. Role 검사 없이 무조건 허용 |
system:serviceaccounts | 클러스터의 모든 SA |
system:serviceaccounts:<ns> | 그 네임스페이스의 모든 SA |
system:masters: 규칙 밖의 그룹
- 따로 기억해 둘 값어치가 있다
- RBAC로 제한할 수 없다 — 규칙 밖에 있기 때문이다
- 클러스터 초기 관리자 인증서에
O=system:masters가 박혀 있다- 인증서는 폐기가 안 되니 한 번 발급하면 만료까지 최고 권한자다
- 두 약점이 곱해지는 조합이라 취급에 주의가 필요하다
system: 접두사가 예약어인 이유
- 쿠버네티스는 인증 시스템이 보내주는 이름을 검증하지 않고 믿는다
- 회사 계정 시스템에서 누군가에게
system:masters그룹을 주면 그대로 클러스터 관리자가 된다 - 이름 충돌 문제가 아니라 권한 상승 경로다
3.4 어드미션 컨트롤 — 내용물을 보는 마지막 검문
세 관문이 보는 것
- 세 번째 관문은 앞의 둘과 보는 것이 다르다
| 관문 | 묻는 것 | YAML |
|---|---|---|
| 인증 | “이 요청 보낸 사람이 누구지?” | 안 봄 |
| 인가 | “그 사람이 pods 를 create 할 자격이 있나?” | 안 봄 |
| 어드미션 | “그래서 그 YAML 에 뭐라고 적었는데?” | 연다 |
어드미션이 잡는 요청: 권한은 있는데 내용이 문제
- 예: 파드를 만들 권한은 분명히 있는 개발자가 검증 안 된 외부 이미지를 쓰려 한다
- 인가는 통과시키고 어드미션이 막는다
두 종류와 순서
- 두 종류가 있고 순서가 있다
- 먼저 다 고쳐놓고 최종 결과물을 검사해야 말이 되니 이 순서다
웹훅으로 붙이는 규칙
- 내장 컨트롤러 외에 웹훅으로 직접 규칙을 붙일 수 있다
ValidatingWebhookConfiguration,MutatingWebhookConfiguration이라는 전용 리소스로 등록한다- 여기서 ABAC와의 대비가 다시 나온다
- 파일 수정 후 재시작이 필요했던 ABAC와 달리 이건 리소스라
kubectl apply로 즉시 반영된다 - 문서 제목이 Dynamic Admission Control인 이유다
- 파일 수정 후 재시작이 필요했던 ABAC와 달리 이건 리소스라
조직 정책이 실제로 강제되는 지점
- “모든 컨테이너는 limit을 명시해야 한다"는 규칙이 문서에만 있으면 아무도 안 지킨다
- 어드미션 컨트롤러에 넣으면 정문에서 자동 집행된다
- 요즘은 코드를 직접 짜기보다 OPA Gatekeeper나 Kyverno로 정책만 선언한다
함정: 웹훅 서버가 죽으면
failurePolicy가 그걸 정한다
failurePolicy | 웹훅이 죽었을 때 |
|---|---|
Fail | 보안은 확실하지만, 웹훅이 죽는 순간 아무것도 배포할 수 없다 |
Ignore | 클러스터는 돌지만, 그동안 정책 검사가 통째로 건너뛰어진다 |
두 선택지의 사고
- 둘 다 나름의 사고를 부른다
- 24장 3.2에서 “CNI가 지원 안 하면 조용히 아무것도 안 막힌다"고 했던 것과 같은 종류의 위험
- 막고 있다고 믿는데 실제로는 안 막히는 상태다
4. Subject — 누구인가
주체: 요청에 결부된 신원
- 주체(subject)는 요청에 결부된 신원이다
- 사람 그 자체가 아니라 요청에 붙어 있는 정보라는 점이 중요하다
- 같은 사람이라도 노트북에서
kubectl을 치면 사람 주체 - CI 파이프라인에서 스크립트를 돌리면 서비스 어카운트 주체
- 같은 사람이라도 노트북에서
- 요청 단위로 판단한다
주체 세 종류
- 종류는 셋이고, 성격이 극단적으로 갈린다
| User | Group | ServiceAccount | |
|---|---|---|---|
| 쿠버네티스 리소스 | ❌ | ❌ | ✅ |
| 실체 | 문자열 | 문자열 목록 | etcd에 저장된 객체 |
kubectl get | 불가 | 불가 | kubectl get sa |
| 오타 검증 | 안 됨 | 안 됨 | 됨 |
| 관리 주체 | 외부 계정 시스템 | 외부 계정 시스템 | 개발자 |
4.1 User — 리소스가 아니라 문자열이다
사용자 객체가 없다
- 쿠버네티스 문서가 직접 못 박는다 — 일반 사용자 계정을 나타내는 객체가 없다
- 전부 없는 명령이다
kubectl get userskubectl create user hongkubectl delete user hong
- 쿠버네티스는 자기 클러스터에 누가 접근할 수 있는지 목록조차 갖고 있지 않다
- 물어봐도 대답을 못 한다
사용자의 실체: 인증 결과의 문자열
- 인증 결과로 나온 문자열이 전부다
- OIDC 토큰의
email이거나 인증서의CN이거나
- OIDC 토큰의
- 그 문자열의 주인이 실존하는지, 퇴사했는지, 심지어 오타인지도 모른다
subjects:
- kind: User
name: 존재하지않는사람 # ← 이래도 apply 성공한다
검증할 방법이 없는 문자열
- 없는 사람을 적어도 apply가 성공하는 건 검증할 방법이 없기 때문이다
- 오타를 내면 조용히 권한이 안 붙고, 원인 찾기가 어렵다
kind: User의 apiGroup
kind: User에 붙는apiGroup: rbac.authorization.k8s.io는 실제 리소스를 가리키는 게 아니다- 인증 결과의 어느 칸을 볼지 정하는 표시다
kind: User면 username 칸kind: Group이면 groups 칸
- 쿠버네티스에서 대상을 가리키는 표준 형식(
apiGroup + kind + name)에 끼워 맞춘 것뿐이다
4.2 ServiceAccount — 파드의 신분증
정확히 나눠야 할 셋
- 오퍼레이터, 파드, ServiceAccount
주체는 파드에 지정된 SA
- 오퍼레이터는 주체가 아니다
- 오퍼레이터를 담은 파드에 지정된 SA가 주체다
- 신분증과 사람을 분리하는 것과 같다
- 파드가 죽고 새로 떠도 같은 신분을 쓴다
- 파드가 10개로 늘어도 권한은 한 번만 주면 된다
비어 있는 리소스
- 리소스 자체는 놀랍도록 비어 있다
apiVersion: v1
kind: ServiceAccount
metadata:
name: random-sa
namespace: default
automountServiceAccountToken: false # ★ 이 예제의 진짜 알맹이
권한이 적히지 않는 SA
spec조차 없다- 권한이 하나도 안 적혀 있다 — 권한은 RoleBinding이 따로 맡기 때문이다
- 이 리소스는 “이런 신분이 존재한다"는 등록 그 이상도 이하도 아니다
역할 분담
- 못 박아두면 뒤가 편하다
| 리소스 | 맡는 것 |
|---|---|
| ServiceAccount | 신분 (who) |
| Role | 권한 (what) |
| RoleBinding | 둘의 연결 |
핵심 함정: automountServiceAccountToken
- 이 절의 핵심 함정이다
- 기본값이
true라, 아무것도 안 하면 켜져 있다 - 모든 네임스페이스에는
default라는 SA가 자동으로 존재한다- 지워도 컨트롤러가 다시 만든다
- SA를 지정하지 않은 파드에 자동으로 붙는다
- 그래서 쿠버네티스 API를 쓸 일이 전혀 없는 이런 파드도,
kind: Pod
spec:
containers:
- name: nginx
image: nginx # 쿠버네티스 API 를 쓸 일이 전혀 없다
뜬 파드에 붙어 있는 토큰
- 실제로 뜬 걸 보면
serviceAccountName: default가 붙어 있고 토큰까지 마운트돼 있다
kubectl exec nginx -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIs...
답: 기본을 뒤집는다
- 개발자가 의도한 게 아니라 기본값이 그런 것이라, 모르면 그냥 넘어간다
- 24장 3.5의 “기본값이 위험한 쪽"과 같은 구조이고, 답도 같다 — 기본을 뒤집는다
신분증을 안 주는 것이 더 강하다
- 권한을 좁히는 것보다 신분증 자체를 안 주는 것이 더 강한 조치다
- 없는 것은 훔칠 수도 없다
SA의 다른 두 필드
- SA에는 신원 외에 두 필드가 더 있다
| 필드 | 용도 | 방향 |
|---|---|---|
imagePullSecrets | 비공개 레지스트리 로그인 정보 | kubelet이 이미지 받아올 때 |
secrets + enforce-mountable-secrets 어노테이션 | 마운트 가능한 Secret 화이트리스트 | 파드가 볼륨 붙일 때 |
imagePullSecrets: 편의 기능
- 파드마다 쓰는 대신 SA에 한 번 붙이면 자동 주입된다
- 그 주입도 어드미션 컨트롤러가 한다
defaultSA에 붙여두는 게 흔한 패턴이다
마운트 가능한 Secret 목록: 보안 기능
- 막으려는 구멍이 흥미롭다
- 파드는 기본적으로 자기 네임스페이스의 아무 Secret이나 마운트할 수 있고, 이건 RBAC로 막히지 않는다
- kubelet이 대신 읽어서 파일로 넣어주는 구조다
- 파드의 SA에 Secret 읽기 권한이 없어도 마운트는 된다
- RBAC를 우회하는 경로가 하나 있다는 뜻이다
실무에서 드문 이유
- 어노테이션 스위치와 목록을 둘 다 켜야 작동한다(하나만 있으면 무효)
- 요즘은 Kyverno 같은 정책 도구로 처리하는 편이라 실무에서 자주 보이진 않는다
4.3 토큰은 어떻게 들어오나 — Secret 에서 projected 로
SA 지정 이후는 전부 자동
- 파드에 SA를 지정하면 그다음은 전부 자동이다
개발자가 쓴 적 없는 볼륨
- 개발자가 쓴 적 없는 볼륨과 볼륨 마운트가 파드 스펙에 들어가 있는 이유가 이것이다
- 3.4의 Mutating 어드미션이 하는 일이 여기서 눈에 보인다
1.22·1.24 전후의 방식 변화
- 1.22에 projected 토큰이 기본이 됐고, 1.24부터는 Secret 토큰을 자동으로 만들지도 않는다
- 이게 이 절에서 알아둘 변화다
| ~1.21 (Secret) | 1.22~ (projected) | |
|---|---|---|
| 저장 위치 | etcd에 Secret으로 | 저장 안 함 — 발급해서 바로 주입 |
| 조회 | kubectl get secret으로 읽힘 | 조회 불가 |
| 만료 | 없음 — 영구 유효 | kubelet이 1시간짜리로 받아, 만료 전 자동 교체 |
| 유효 범위 | 무제한 | 파드 생명주기 동안만 |
1시간 만료의 단서
- kubelet은 1시간짜리를 요청하고 1시간마다 새로 받는다
- 다만 API 서버의
--service-account-extend-token-expiration(기본 true)이 켜져 있으면 발급된 토큰 자체는 최대 1년까지 유효하다- 옛 방식에서 넘어올 때 토큰을 캐시하던 앱이 갑자기 깨지지 않게 하려는 장치다
핵심 개선: 중간 표현이 사라졌다
- 예전엔 토큰이 Secret이라는 실물로 클러스터에 저장돼 있었다
- 파드에 들어가지 않고도
secrets: list권한만 있으면 네임스페이스의 모든 토큰을 조용히 긁어갈 수 있었다
오해 주의: 노출 차단이 아니라 경로 축소
- 이건 토큰 노출을 막는 게 아니라 경로를 줄인 것이다
- 여전히 남아 있는 경로가 있다
| 경로 | Secret 방식 | projected |
|---|---|---|
| 파드 침입 | 가능 | 가능 |
kubectl exec | 가능 | 가능 ← pods/exec 권한은 사실상 토큰 읽기 권한이다 |
| 노드 침입 | 가능 | 가능 |
| Secret 읽기 | 가능 | 불가 ← 사라진 것 |
| etcd / 백업 | 가능 | 불가 ← 사라진 것 |
개선의 의미: 시한부 토큰
- 사라진 둘이 가장 조용하고 넓은 경로였다
- 여기에 만료와 파드 종속이 겹치니, 어느 경로로 훔쳐도 시한부가 된다
- 단, 기본 설정에선 만료가 최대 1년까지 늘어나니 실제로 수명을 끊는 건 파드 종속 쪽이다
실전 함정: 토큰 파일 캐시
- 자동 교체되므로 프로그램이 토큰 파일을 한 번만 읽고 캐시하면 안 된다
- 증상: 만료 연장을 끈 클러스터에서 잘 돌던 앱이 몇 시간 뒤 401을 뱉는다
- 공식 클라이언트 라이브러리는 알아서 다시 읽는다
만료 없는 토큰이 필요할 때
- 예: 클러스터 밖 CI 도구
- Secret을 수동으로 만들 수는 있지만, 옛 방식의 단점을 그대로 떠안는다
- 요즘 권장은
kubectl create token my-sa --duration=1h- 수명을 지정해 받고 저장하지 않는다
4.4 Group — 명단은 클러스터 밖에 있다
사람 쪽을 묶는 그룹
- Role이 권한 쪽을 묶는다면, 그룹은 사람 쪽을 묶는다
- 양쪽에서 묶어야 관리가 완성된다
역할로 부르기
- 24장 3.4의 “역할 출입증” 라벨링과 발상이 정확히 같다
- 이름을 직접 부르지 않고 역할로 부른다
그룹 멤버십 구조의 부재
- 그런데 쿠버네티스에 그룹 멤버십 구조가 없다
- kind: Group
name: developers
# members: [...] ← 이런 필드는 없다
명단을 가진 쪽: 인증 시스템
developers에 누가 속하는지 쿠버네티스는 모른다- 그 판단은 인증 시스템이 이미 끝냈고, 결론만 토큰에 실려 온다
{ "email": "alice@company.com", "groups": ["developers", "oncall"] }
RBAC가 하는 일: 문자열 대조
- “이 사용자가 그 그룹에 속하나?“를 검사하는 게 아니다
- 요청에 딸려온 그룹 문자열 목록에 그 이름이 들어 있는지 대조할 뿐이다
콘서트장 입구 비유
- 직원은 VIP 명단을 갖고 있지 않다
- 팔찌에 “VIP” 라고 적혀 있는지만 본다
- VIP 가 몇 명인지, 누구인지 모른다
- 명단은 티켓 판매처에 있다
따라 나오는 두 가지
- 소속을 바꿔도 즉시 반영되지 않는다
- 회사 계정에서 빼도 이미 발급된 토큰에는 그룹이 박혀 있다 → 만료까지 유효
- 토큰을 위조할 수 있으면 아무 그룹이나 될 수 있다
- 서명 검증이 신뢰의 유일한 뿌리다
서비스 어카운트 그룹: 이름에서 계산
- 서비스 어카운트 그룹은 예외적으로 규칙이 있다
- 명단이 있어서가 아니라 이름에서 계산되기 때문이다
SA 그룹의 범위 주의
- 지정할 필요 없이 자동으로 붙는다
- “dev의 모든 파드에게"를 한 줄로 쓸 수 있다
- 범위가 넓어 새 파드가 생기면 자동으로 권한을 갖게 되니 조심해야 한다
| 사람 그룹 | SA 그룹 | |
|---|---|---|
| 어디서 오나 | 회사 계정 시스템 | 이름에서 자동 계산 |
| 자유롭게 정의 | 가능 | 불가 (규칙 고정) |
| 멤버 조회 | 불가 | kubectl get sa -n dev |
username이 필요한 이유
- 권한을 그룹으로 준다면 개인 이름은 무의미해 보인다
- 두 용도가 남는다
- 개인에게 직접 주는 경우
- 감사 로그
- 프로덕션 파드가 삭제됐는데 로그에
developers만 있고 이름이 없다면 팀원 20명 중 누가 했는지 영영 모른다
| 필드 | 답하는 것 |
|---|---|
| groups | 뭘 할 수 있나 (권한) |
| username | 누가 했나 (책임) |
5. RBAC — 무엇을 할 수 있는가
인가 방식: 사실상 RBAC
- 인가 방식도 플러그형이라 갈아 끼울 수 있다(RBAC / ABAC / 웹훅 / Node)
- 하지만 결론은 이미 나 있다 — 거의 모든 클러스터가 RBAC를 쓴다
ABAC가 밀려난 이유
- 이 책의 다른 장들과도 통한다
| ABAC | RBAC | |
|---|---|---|
| 저장 | 노드의 파일 | etcd (API 리소스) |
| 변경 반영 | 서버 재시작 | 즉시 |
| 관리 | SSH + 수동 편집 | kubectl, GitOps |
| 조회 | 파일 열어보기 | kubectl get roles |
선언적 운영 밖의 ABAC
- ABAC만 혼자 선언적 운영 철학 밖에 있었다
- 24장에서 NetworkPolicy가 “앱 옆의 YAML"이었던 것처럼, RBAC도 앱과 함께 배포되는 리소스다
- 그래서 이게 개발자의 일이 된다
5.1 Role — 세 축으로 권한을 쪼갠다
규칙의 세 축
- Role의 알맹이는
rules하나뿐이고, 각 규칙은 세 축으로 되어 있다
# Example 26-6. 코어 리소스 읽기 전용
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer-ro
namespace: default # ❶ Role 은 항상 네임스페이스에 묶인다
rules:
- apiGroups:
- "" # ❷ 빈 문자열 = 코어 API 그룹
resources: # ❸ 무엇을
- pods
- services
verbs: # ❹ 어떻게
- get
- list
- watch
| 축 | 정하는 것 |
|---|---|
apiGroups | 어느 API 그룹의 |
resources | 어떤 리소스를 |
verbs | 어떻게 |
가장 헷갈리는 부분: apiGroups: [""]
- 쿠버네티스 리소스는 그룹으로 나뉘어 있다
- 초창기 리소스들은 그룹 개념이 생기기 전에 만들어져 이름이 없다
- 그 “이름 없음"이 빈 문자열이다
| 그룹 | 리소스 |
|---|---|
"" (코어) | pods, services, configmaps, secrets, serviceaccounts, nodes |
apps | deployments, statefulsets, daemonsets |
batch | jobs, cronjobs |
rbac.authorization.k8s.io | roles, rolebindings |
그룹을 잘못 적으면: 조용히 무효
- 그래서 파드와 Deployment 권한은 규칙을 나눠 써야 한다
- 그룹을 잘못 적으면 에러도 안 나고
apply는 성공하는데 조용히 아무 효력이 없다 - “권한 줬는데 왜 안 되지?“의 십중팔구가 이것이다
kubectl api-resources로 확인하는 습관이 필요하다
verb: HTTP보다 잘게 쪼갠 동작
- verb는 HTTP 메서드에 대응되지만 더 잘게 쪼개져 있다
| verb | HTTP | 주의점 |
|---|---|---|
get | GET | 이름을 알아야만 조회 |
list | GET | 뭐가 있는지 모르는 상태에서 전부 훑는다 |
watch | GET | 연결을 열어두고 변화 알림 — 오퍼레이터 필수 |
create | POST | |
update / patch | PUT / PATCH | 따로 줘야 한다 |
delete | DELETE | |
deletecollection | DELETE | 한 번에 전부 삭제 — 매우 위험 |
GET을 셋으로 나눈 이유: 위험도
- HTTP로는 같은 GET인데 셋으로 나눈 이유는 위험도가 다르기 때문이다
secrets: get은 이름을 아는 하나secrets: list는 네임스페이스의 모든 Secret을 통째로 긁는다
실전 함정: get과 list
get만 주면kubectl get pods가 안 된다 — 목록 조회는list다- 오퍼레이터에는 거의 항상
get, list, watch세트가 들어간다
하위 리소스는 별도 항목
resources: ["pods", "pods/log", "pods/exec"]
하위 리소스의 함정
pods를 적어도pods/log는 안 포함된다- “파드는 보이는데 로그가 안 보인다"가 여기서 나온다
pods/exec는 이름만 봐서는 안 위험해 보이는데 컨테이너 안으로 들어가는 권한이다- 4.3에서 봤듯 그 네임스페이스의 모든 토큰을 읽을 수 있다
이 절의 핵심 철학: 거부 규칙이 없다
- 24장 3.3과 똑같다
- RBAC 에는 거부(deny) 규칙이 없다
- 기본이 전부 거부고, 적은 것만 열린다
적지 않은 것은 금지된 것
- 위 예제가
delete를 금지한 게 아니라 안 적은 것이다 - “전부 허용하되 삭제만 금지"는 불가능하다
- 대신 규칙을 읽으면 할 수 있는 일의 전부가 그대로 보이고, 우선순위 계산이 필요 없다
규칙 여러 개는 OR
- 하나만 걸려도 통과한다
- 그래서 세밀하게 잘 짜놓고 마지막에 넓은 규칙 하나를 추가하면 앞의 것들이 전부 무의미해진다
와일드카드: 그 위험의 완성형
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"] # 사실상 cluster-admin
*가 특히 위험한 이유: 미래까지 포함
*는 미래까지 포함한다- 나중에 CRD를 설치하면 권한을 준 시점에 존재하지도 않던 리소스에 접근하게 된다
- 24장 3.6의
namespaceSelector: {}가 “전부"였던 것과 같은 종류의, 무심코 붙였다가 범위가 폭발하는 표기다
resources: ["*"]의 실제 범위
resources: ["*"]는 무제한이 아니다- 지정한
apiGroups안에서만 전부다 - 진짜 무제한은 셋이 모두
*일 때다
5.2 RoleBinding — 권한을 붙인다는 것의 의미
Role만으로는 아무 일도 없다
- Role을
apply해도 아무 일도 일어나지 않는다 - 아직 아무에게도 안 붙었기 때문이다
# Example 26-7. RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-rolebinding
namespace: default # 생략하면 default. 명시하는 편이 안전
subjects: # ❶ 목록 — 여러 개 가능
- kind: User
name: alice
apiGroup: "rbac.authorization.k8s.io"
- kind: ServiceAccount # ❷ 종류가 달라도 한 목록에 섞인다
name: contractor
apiGroup: ""
roleRef: # ❸ 하나만. 변경 불가
kind: Role
name: developer-ro
apiGroup: rbac.authorization.k8s.io
주체와 Role: 다대다
- 주체와 Role은 다대다다
subjects는 배열이고roleRef는 단수다- 한 주체에 여러 Role을 주려면 RoleBinding을 여러 개 만든다
roleRef 변경 불가: 의도된 설계
roleRef가 변경 불가인 게 의도적이다- “파드 읽기"용 바인딩이 어느 날 슬쩍 “전체 관리자"로 바뀌는 사고를 막는다
- 수정하려면 지우고 다시 만들어야 한다
“붙인다"의 정확한 뜻: 주체는 변하지 않는다
- “붙인다"는 표현이 오해를 만들 수 있어 정확히 해두면, 주체는 전혀 변하지 않는다
kubectl create sa my-operator
kubectl apply -f rolebinding.yaml
kubectl get sa my-operator -o yaml # 권한에 대한 언급이 하나도 없다
권한이 기록되는 곳: RoleBinding
- 권한은 SA 안에 저장되는 게 아니라 RoleBinding이라는 별도 리소스에 기록돼 있다
- API 서버는 요청마다 그것들을 훑어서 계산한다
- 그래서 권한 회수는 SA를 지우는 게 아니라 연결선만 끊으면 된다
| 표현 | 실제 |
|---|---|
| 권한을 붙인다 | RoleBinding 생성 |
| 권한을 뗀다 | RoleBinding 삭제 |
| 권한을 바꾼다 | Role의 rules 수정 |
권한은 모든 경로에서 합쳐진다
- 24장 3.3에서 본 것과 같은 이유로, 권한은 모든 경로에서 합쳐진다
흔한 실수: 개인 바인딩만 지우기
- 특정 사용자의 권한을 줄이려고 그의 RoleBinding을 지운다
- 그런데 그룹으로 들어오는 권한이 남아 여전히 가능한 경우가 흔하다
5.3 네 가지 조합 — 범위는 바인딩이 정한다
바인딩과 권한 정의의 네 조합
- Role은 네임스페이스에 묶인다
- 그래서 짝이 되는 클러스터 범위 리소스가 따로 있고, 조합이 넷이다
| 바인딩 | 권한 정의 | 적용 범위 |
|---|---|---|
| RoleBinding | Role | 그 네임스페이스에만 |
| RoleBinding | ClusterRole | 그 네임스페이스에만 (정의만 재사용) |
| ClusterRoleBinding | ClusterRole | 클러스터 전체 |
| ClusterRoleBinding | Role | ❌ 불가능 |
네 번째가 안 되는 이유
- 이 구조를 이해하는 열쇠다
- ClusterRoleBinding은 “모든 네임스페이스에 적용"인데 Role은 한 네임스페이스분의 권한밖에 없다
- 바인딩이 요구하는 범위를 권한이 채울 수 없다
가장 헷갈리는 것: ClusterRole의 뜻
- ClusterRole 은 “클러스터 전역 권한"이 아니라 “클러스터 전역에서 참조 가능한 정의"다
- 범위는 바인딩이 정한다
- 같은 ClusterRole을 RoleBinding으로 붙이면 그 네임스페이스에만
- ClusterRoleBinding으로 붙이면 전체에 적용된다
ClusterRole의 두 용도
- 네임스페이스가 없는 리소스를 다루려고 — 선택의 여지가 없다
- nodes, namespaces, CustomResourceDefinitions, persistentvolumes, storageclasses, clusterroles
- 여러 네임스페이스에서 재사용하는 템플릿 — 선택의 문제
- Role 은 네임스페이스를 못 넘어 열 곳에 쓰려면 열 번 복사해야 한다
첫 번째 용도의 CRD
- 첫 번째 용도에 CRD가 있는 게 중요하다
- 오퍼레이터를 설치하려면 CRD 권한이 필요하고, 그건 ClusterRole로만 줄 수 있다
customresourcedefinitions: create는 사실상 API를 확장할 수 있는 권한이다- 그래서 개발자에게는 보통 읽기만 준다
기본 제공 ClusterRole 넷
- 직접 짜지 않아도 된다
| ClusterRole | 범위 | 빠진 것 |
|---|---|---|
view | 읽기 | Secret, Role, RoleBinding |
edit | 읽기+수정 | Role, RoleBinding |
admin | 네임스페이스 내 거의 전부 | ResourceQuota·네임스페이스 자체 쓰기 |
cluster-admin | 전부 | — |
빠진 것의 이유
view에서 Secret이 빠진 이유는 4.3의 논리 그대로다- 읽는 순간 DB 비밀번호와 클라우드 키를 손에 넣으니 “구경만” 하는 권한이 아니다
edit에서 Role/RoleBinding이 빠진 건 권한을 수정할 수 있으면 스스로 권한을 늘릴 수 있어서다
ClusterRoleBinding은 관리 작업에만
- ClusterRoleBinding은 관리 작업에만 써야 한다
- 책이 명시적으로 못 박는 부분이다
- 이유: “앞으로 생길 네임스페이스까지 자동으로 포함"하기 때문이다
*와일드카드가 미래 리소스를 포함했던 것과 같은 성질이다
오퍼레이터의 정답에 가까운 패턴
subjects에는 SA만namespace필드를 가질 수 있다는 점을 이용한다
kind: RoleBinding
metadata:
namespace: prod # 권한이 적용되는 곳
subjects:
- kind: ServiceAccount
name: my-operator
namespace: operators # 오퍼레이터가 사는 곳 — 달라도 된다
roleRef:
kind: ClusterRole
name: view-pod
관리할 네임스페이스마다 RoleBinding 하나씩
- 오퍼레이터를 한 곳에 두고 관리할 네임스페이스마다 RoleBinding을 하나씩 만든다
- YAML은 늘지만 권한은 딱 그 네임스페이스들에만 열린다
- 새 네임스페이스가 생겨도 자동으로 퍼지지 않는다
- 그 명시성이 안전장치가 된다
ClusterRoleBinding으로 바꾸면
- 이 그림을 ClusterRoleBinding 으로 한 줄 바꾸면 어떻게 되는지가 다음이다
5.4 집계 — 파이프는 그대로 두고 물통을 채운다
기본 view에 없는 CRD
- 내가 CRD를 만들면 사용자는 그걸 못 본다
- 기본
view에 안 들어 있기 때문이다
kubectl get postgresclusters
# Error: postgresclusters is forbidden
view를 직접 고치지 않는 이유
view를 직접 고치면 되지 않나 싶지만 쿠버네티스 업그레이드 때 덮어쓰인다- 그래서 집계(aggregation)라는 장치가 있다
# 기본 view 는 실제로 이렇게 생겼다
kind: ClusterRole
metadata:
name: view
labels:
rbac.authorization.k8s.io/aggregate-to-edit: "true" # ❶ 나를 edit 이 가져가라
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.authorization.k8s.io/aggregate-to-view: "true" # ❷ 이 라벨 붙은 걸 내가 모은다
rules: [] # ❸ 쿠버네티스가 채운다
rules: []의 뜻: 맡긴 것
- **
rules: []는 “규칙이 없다"가 아니라 “내가 안 쓸 테니 네가 채워라”**다 - 실제로 조회하면 수십 줄이 들어 있다
- 여기에 수동으로 뭘 적어도 컨트롤러가 곧 덮어쓴다
defaultSA를 지워도 다시 생기는 것과 같은, “쿠버네티스가 소유한 필드"다
자동으로 쌓이는 계단
- 라벨과 셀렉터가 양쪽에 다 있어서 계단이 자동으로 쌓인다
맨 아래 하나로 전파
- 누가 “edit에는 view 권한도 포함"이라고 적어둔 게 아니다
- 그래서 맨 아래 하나만 추가해도 위로 전부 전파된다
라벨+셀렉터: 관계의 방향을 뒤집는다
- 라벨+셀렉터라는 방식 자체는 24장 3.4에서 이미 본 것이다
- Service가 파드를 찾는 그 원리다
- 관계의 방향을 뒤집는 효과가 핵심이다
| 방식 | 누가 움직이나 | 결과 |
|---|---|---|
| 목록 방식 | 수집하는 쪽이 대상을 다 알아야 한다 | view 를 계속 고쳐야 함 |
| 라벨 방식 | 수집당하는 쪽이 손을 든다 | view 는 그대로 |
오퍼레이터 개발자가 할 일: 이 한 장
kind: ClusterRole
metadata:
name: postgres-viewer
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
- apiGroups: ["mycompany.com"]
resources: ["postgresclusters"]
verbs: ["get", "list", "watch"]
바인딩이 없는 이유
- 바인딩이 없다는 점을 눈여겨볼 만하다
- 필요 없기 때문이다
파이프는 그대로, 물통만 채운다
- 파이프는 그대로 두고 물통을 채우니, 기존
view사용자들이 아무 설정 없이 새 CRD를 볼 수 있게 된다 - 오퍼레이터 개발자는 어느 회사가 어떤 그룹을 어떻게 묶어놨는지 알 수 없다
- 그래서 바인딩은 안 만들고 권한 내용만 제공하는 게 맞는 설계다
5.5 디버깅 — 추측하지 말고 물어봐라
직접 물어보는 API
- 권한이 여러 경로에서 합쳐지고 거부 규칙도 없다
- 최종 결과를 머리로 계산하는 건 사실상 불가능하다
- 그래서 직접 물어보는 API가 있다
kubectl auth can-i list pods \
--namespace dev-1 --as system:serviceaccount:test:test-sa
# yes
--as로 남인 척하기
--as로 남인 척할 수 있다- SA는 4.4의 정식 긴 이름을 쓴다
내부 동작: SubjectAccessReview
- 내부적으로는
SubjectAccessReview라는 리소스가 생성되고, 인가 컨트롤러가status를 채워 돌려준다 - 평소 쓰는 판단 로직을 그대로 돌리되 실행만 안 하는 것이라 결과가 정확하다
- 저장은 되지 않는다 — 리소스 형식을 빌린 함수 호출에 가깝다
한계: 이유는 알려주지 않는다
can-i는 결과만 알려주고 이유는 알려주지 않는다- 사실 이건 RBAC 구조상 어쩔 수 없다
- 거부 규칙이 없으니 안 되는 이유는 언제나 하나다 — 허용하는 규칙을 못 찾았다
- 특정 규칙이 막은 게 아니라 아무것도 허용하지 않은 것이라, 지목할 범인이 없다
질문 바꾸기
- ❌ 왜 안 되지?
- ✅ 지금 뭐가 되지? →
kubectl auth can-i --list -n dev --list가 여러 경로에서 합쳐진 최종 권한 전부를 보여준다- 거기서 빠진 걸 찾는 방식이다
전체 그림을 보는 도구
can-i는 한 번에 하나씩만 물어보니, 전체 그림이 필요하면 도구를 쓴다
| 상황 | 도구 |
|---|---|
| 이 권한 되나 확정 | kubectl auth can-i |
| 이 주체의 전체 권한 | kubectl auth can-i --list, rakkess (kubectl access-matrix) |
| 클러스터에 위험한 게 있나 | KubiScan — cluster-admin, *, secrets: list, pods/exec 스캔 |
| 내가 누구로 인식되나 | kubectl auth whoami |
kubectl auth whoami의 쓰임
- 마지막 것이 의외로 자주 쓰인다
- 3.2의 “인증 순서가 고정되지 않았다” 문제로 엉뚱한 username으로 인식되고 있을 때
- 이걸 안 보면 RBAC만 계속 뒤지게 된다
6. Discussion
6.1 세 가지 조언 — 필요 없는 건 주지 마라
책의 세 조언: 전부 같은 말의 변주
- 와일드카드를 피하라
verbs: ["*"]는deletecollection과pods/exec까지 포함하고, 미래 리소스도 포함한다- → “와일드카드 금지"를 정책으로 세우고, 타당한 이유가 있을 때만 예외로 푼다
- 그때그때 판단하면 결국 편한 쪽으로 흐른다
- 파드에 cluster-admin 을 주지 마라. 절대로
- 책에서 “Never.” 를 문장 하나로 따로 쓴 유일한 대목
- 권한 수정 가능 → 스스로 무한 확장
- 모든 Secret 열람 → 클러스터 밖까지
- 사람 관리자와 달리 파드는 코드라 취약점이 있을 수 있다
- 토큰을 자동 마운트하지 마라
automountServiceAccountToken: false- 기준은 하나 — 이 파드가 쿠버네티스 API 를 호출하나?
세 조언의 공통점: 이 패턴의 요약
| 조언 | 뜻 |
|---|---|
| 와일드카드 | 필요 없는 권한을 주지 마라 |
| cluster-admin | 필요 없는 최고 권한을 주지 마라 |
| 토큰 마운트 | 필요 없는 신분증을 주지 마라 |
효과 순서: ② → ③ → ①
- ②는 하나만 지켜도 최악을 피한다
- ③은 대부분의 파드에 즉시 적용 가능하다
- ①은 꾸준히 지켜야 하는 습관이다
조합 선택: 좁은 것에서 시작
- 조합 선택은 좁은 것에서 시작해 필요할 때만 넓히는 순서다
6.2 24장과의 구분 — 조작 권한 vs 통신 권한
같은 클러스터의 두 상황
- 24장 5.1에서 이미 짚었지만, 26장을 읽고 나면 경계가 더 선명해진다
| RBAC (26장) | NetworkPolicy / AuthorizationPolicy (24장) | |
|---|---|---|
| 요청 목적지 | API 서버 | 내 앱 파드 |
| 무엇을 통제 | 쿠버네티스 리소스 조작 | 파드 간 통신 |
| 비유 | 건물 관리 권한 — 방을 만들고 부수는 것 | 방끼리 왕래 규칙 |
| 신원의 근거 | 토큰의 username·groups | IP(NetworkPolicy) / 인증서(mTLS) |
둘은 서로 만나지 않는다
- 민수에게 RBAC 전권을 줘도 그가 frontend 파드 안에서
DELETE /orders를 날리면 AuthorizationPolicy가 막는다 - AuthorizationPolicy가 아무리 빡빡해도
kubectl delete pod는 RBAC 영역이다
만나는 한 곳: ServiceAccount
- 흥미로운 건 한 곳에서 만난다는 점이다
| 체계 | ServiceAccount의 쓰임 |
|---|---|
| RBAC | SA 에 Role 을 붙여 “쿠버네티스에 뭘 할 수 있나"를 정한다 |
| Istio mTLS | SA 이름이 그대로 신원이 되어 “누구와 통신할 수 있나"의 근거가 된다 — cluster.local/ns/chili-shop/sa/frontend |
같은 신분증, 다른 문
- 같은 신분증이 두 체계에서 각각 다른 문을 여는 셈이다
- 24장 4.2에서 “ServiceAccount는 파드가 쓰는 계정"이라고 짧게 넘어갔던 것의 정체가 26장 전체였다
6.3 모든 것이 리소스다 — 질문조차
관통하는 설계 철학
- 쿠버네티스는 모든 것을 API 리소스로 표현하려 한다
| 대상 | 리소스 | 비고 |
|---|---|---|
| 권한 | Role, RoleBinding | ABAC 의 파일과 대비 |
| 신분 | ServiceAccount | |
| 어드미션 규칙 | ValidatingWebhookConfiguration | “Dynamic” Admission Control |
| 토큰 요청 | TokenRequest | |
| “이거 되나요?” | SubjectAccessReview | ← 질문조차 리소스다 |
리소스가 된다는 것
kubectl apply로 다루고, Git에 커밋하고, 코드 리뷰를 받고, 감사 로그에 남는다는 뜻이다- 전부 같은 문장에서 나온다
- ABAC가 밀려난 이유
- 어드미션 웹훅이 “dynamic"인 이유
can-i가 특별한 명령이 아닌 이유
남은 예외 둘: User와 Group
- 예외가 둘 남아 있다 — 이 둘만 리소스가 아니고 문자열이다
- 의도적인 선택이다
- 신원 관리는 회사가 이미 잘하고 있다
- 쿠버네티스가 또 하면 계정을 두 벌 관리하게 된다
| 대상 | 신분증 | 형태 |
|---|---|---|
| 쿠버네티스가 만든 것(파드) | 신분증도 자기가 발급한다 | ServiceAccount (리소스) |
| 쿠버네티스가 안 만든 것(사람) | 신분증은 남에게 맡긴다 | User (문자열) |
비대칭 하나가 설명하는 것
- 이 비대칭 하나가 26장의 거의 모든 구조를 설명한다
- 왜 인증 방식이 다섯 개나 되는지
- 왜 사람은 조회가 안 되는지
- 왜 그룹 명단이 클러스터 밖에 있는지
- 왜 서비스 어카운트만 모든 클러스터에서 똑같이 동작하는지
핵심 메시지
Access Control 의 몫: 하나뿐인 문에서, 뚫렸을 때 닿을 범위를 미리 좁히는 것
- 전제 → API 서버가 유일한 문. 모든 요청이 인증→인가→어드미션 셋을 순서대로 지난다
- 401=인증, 403=인가, admission webhook 메시지=어드미션
- 인증 → 쿠버네티스에 계정 시스템이 없다. 회사 것을 꽂는다
- 다섯 방식 중 하나만 통과하면 되니, 방식을 늘리는 건 문을 늘리는 것
- 결과물은 언제나 username + groups (UID·extra 는 RBAC 가 안 본다)
- 주체 → User·Group 은 문자열, ServiceAccount 만 리소스
- SA 는 권한이 아니라 신분. 권한은 RoleBinding 이 따로 맡는다
- 토큰 → 1.22 부터 projected — 저장 안 하고, 1시간마다 교체, 파드 죽으면 무효
- 그래도 exec·노드 침입 경로는 남는다. 근본 대책은 automount: false
- RBAC → 세 축(apiGroups·resources·verbs)으로 쪼갠다. 거부 규칙이 없다
- 규칙은 OR, 권한은 모든 경로에서 합집합, 빼는 방법이 없다
- 범위 → ClusterRole 은 “정의"일 뿐, 범위는 바인딩이 정한다
- ClusterRoleBinding 은 미래 네임스페이스까지 포함 — 관리 작업에만
- 집계 → rules: [] 는 비운 게 아니라 맡긴 것. 라벨만 붙이면 view 가 흡수한다
- 디버깅 → 추측하지 말고 can-i. 단 이유는 안 알려주니 –list 로 “뭐가 되는지"를 본다
- 조언 → * 쓰지 말고, 파드에 cluster-admin 절대 주지 말고, 안 쓰면 토큰 끄기
Access Control 은 “침해를 막는 패턴이 아니라, 침해의 반경을 정하는 패턴” 이다. 문은 하나뿐이니 그 문만 지키면 되고, 신분은 밖에서 오지만 권한은 안에서 정하며, 거부를 적을 수 없으니 필요한 것만 하나씩 적어야 한다. 그러면 파드 하나가 뚫려도, 그 파드가 할 수 있는 일은 원래 해야 했던 일뿐이다.
7. References
"Kubernetes does not have objects which represent normal user accounts."
rules의 세 축(5.1), 규칙 간 OR 평가와 거부 규칙의 부재, Role/ClusterRole × RoleBinding/ClusterRoleBinding 네 조합 중 ClusterRoleBinding + Role이 불가능하다는 점(5.3), 그리고 aggregationRule을 설정하면 rules가 컨트롤러 소유가 된다는 점(5.4)을 정의한다. 기본 제공 ClusterRole view/edit/admin의 정확한 권한 범위도 여기 있다.