0. Contents


1. Overview

Access Control 패턴의 핵심"뚫리는 건 막을 수 없다. 뚫렸을 때 닿는 범위를 미리 줄인다"유일한 문API 서버 = 클러스터의 유일한 문kubectl 도, 오퍼레이터도, 스케줄러 자신도 여기로만 들어온다. 뒷문이 없다1관문인증 (Authentication) — "너 누구야?"OIDC / X.509 / 프록시 / 정적 토큰 / 웹훅 — 다섯 중 하나만 통과하면 끝쿠버네티스에는 계정 시스템이 없다. 회사가 쓰던 것을 콘센트처럼 꽂는다어느 문으로 들어왔든 결과물은 한 형태: username + groups2관문인가 (Authorization) — "그거 해도 돼?"RBAC = Role(권한 목록) + RoleBinding(연결선), 둘은 분리돼 있다거부 규칙이 없다. 허용만 쌓이고, 한 군데서만 걸려도 통과막히면 403 Forbidden개발자의 영역3관문어드미션 컨트롤 (Admission Control) — "내용이 규칙에 맞아?"앞의 둘과 달리 YAML 안을 열어본다Mutating(고쳐 넣음) → Validating(검사)셋을 다 통과해야 비로소 etcd 에 기록된다 🔒
뚫렸을 때 닿는 범위를 줄이는 세 관문

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 Unauthorized403 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를 확장하는 컨트롤러·오퍼레이터가 하는 일
    1. 클러스터 전역을 계속 watch — “어디든 내 CRD 가 생기면 알려줘”
    2. 변화를 감지하면 Pod, Service, Secret 을 직접 만들고 지운다
    3. 이걸 한 네임스페이스가 아니라 여러 곳에서 한다
  • 즉 오퍼레이터는 태생적으로 넓은 권한을 요구한다
    • 설정 실수가 아니라 패턴 자체의 성질이다
  • Prometheus Operator, cert-manager처럼 남이 만든 것을 가져다 쓰는 경우가 대부분이다
    • 코드를 다 읽어보지 않은 프로그램에 클러스터 조작 권한을 주는 셈이 된다

책의 답: 영향 제한

  • “권한을 주지 마라"가 아니다 — 그러면 일을 못 한다
  • 잘게 쪼개서 최소한만 주고, 뚫렸을 때 피해 범위를 좁혀두라는 것이다
  • 원문의 표현은 “limit the impact of any potential security breaches”
    • 침해를 막는 게 아니라 영향을 제한한다
  • 24장의 blast radius 축소와 같은 사고방식이다

이 장의 목표

  • 모든 요청이 지나는 단 하나의 문에서, 개발자가 앱과 함께 배포하는 권한 규칙으로, 뚫렸을 때 닿을 수 있는 범위를 미리 좁히는 것

3. 세 관문 — 모든 요청이 지나는 길

3.1 API 서버는 문이 하나다

모든 요청이 지나는 세 단계

  • 쿠버네티스 API 서버로 들어오는 모든 요청은 세 단계를 순서대로 지난다
kubectl오퍼레이터 · 스케줄러API 서버인증너 누구야?인가그거 해도 돼?어드미션뭐라고 썼는데?✗ 401✗ 403✗ 403 + 웹훅앞에서 막히면 뒤는 실행조차 되지 않는다etcd기록우회로가 없다 — kubectl 이든 파드 안 오퍼레이터든 내부 컴포넌트든 같은 문을 지난다
세 관문 — 인증 · 인가 · 어드미션을 순서대로 통과해야 기록된다

그림의 요점: 바깥 테두리

  • 정작 중요한 건 세 상자가 아니라 바깥 테두리다
  • 세 관문이 전부 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이 다를 수 있다(hong vs hong@company.com)
    • RBAC는 그 문자열로 판단하니 권한이 달라진다
  • 코드도 설정도 안 바꿨는데 어제 되던 게 오늘 403이 나는, 원인 찾기 지독한 버그다
  • 해법은 단순하다 — 클라이언트 하나에 자격 증명 하나만

3.3 사용자 표현 — 인증이 남기는 것

결과물은 하나의 형태

  • 방식은 다섯 개인데 결과물은 하나의 형태로 통일된다
  • 추출 방법은 전부 다르지만(JWT 파싱, 인증서 필드 읽기, 헤더 읽기, CSV 조회, 외부 호출) 나오는 것은 같다
인증 방식 다섯 가지 — 추출 방법은 전부 다르다OIDC 토큰JWT 파싱X.509 인증서인증서 필드 읽기인증 프록시헤더 읽기정적 토큰 파일CSV 조회웹훅외부 호출결과물은 하나의 형태 — 사용자 표현usernamealiceRBAC가 봄UID4bc01e30-406b-4514RBAC는 안 봄groups[system:authenticated,developers]RBAC가 봄extra{scopes:openid}RBAC는 안 봄인증이 끝나면 토큰은 사라진다 — 뒷단계는 어떤 방식으로 인증됐는지 모른다
다섯 개의 입구, 하나의 출구
필드RBAC가 쓰나용도
username✅권한 판단, 감사 로그
UID❌이름 재사용 구분 — 퇴사한 alice와 새로 온 alice
groups✅권한 판단
extra❌인가 웹훅용 부가 정보

RBAC가 보는 것은 둘뿐

  • RBAC가 실제로 보는 건 두 개뿐이다
  • 인증이 끝나는 순간 토큰은 사라진다
    • 뒷단계는 어떤 방식으로 인증됐는지 모르고, 알 필요도 없다

설계 효과: 서로의 내부를 모르는 구조

  • 인증 방식을 추가해도 RBAC를 손댈 필요가 없다
  • 반대로 인가를 웹훅으로 바꿔도 인증은 그대로다
  • 24장 5.2에서 본 컨트롤 플레인과 데이터 플레인의 분리처럼, 정해진 규격으로만 소통하고 서로의 내부는 모른다

쿠버네티스가 자동으로 붙이는 그룹

  • groups에 system:authenticated가 섞여 있는 게 눈에 띈다
  • 어느 계정 시스템에도 없는, 쿠버네티스가 자동으로 붙이는 그룹이다
그룹언제 붙나
system:unauthenticated자격 증명 없이 온 요청 — 여기 권한 주면 익명 접근이 열린다
system:authenticated어떤 방식으로든 인증 성공한 모든 요청
system:mastersRBAC를 아예 건너뛴다. Role 검사 없이 무조건 허용
system:serviceaccounts클러스터의 모든 SA
system:serviceaccounts:<ns>그 네임스페이스의 모든 SA

system:masters: 규칙 밖의 그룹

  • 따로 기억해 둘 값어치가 있다
  • RBAC로 제한할 수 없다 — 규칙 밖에 있기 때문이다
  • 클러스터 초기 관리자 인증서에 O=system:masters가 박혀 있다
    • 인증서는 폐기가 안 되니 한 번 발급하면 만료까지 최고 권한자다
  • 두 약점이 곱해지는 조합이라 취급에 주의가 필요하다

system: 접두사가 예약어인 이유

  • 쿠버네티스는 인증 시스템이 보내주는 이름을 검증하지 않고 믿는다
  • 회사 계정 시스템에서 누군가에게 system:masters 그룹을 주면 그대로 클러스터 관리자가 된다
  • 이름 충돌 문제가 아니라 권한 상승 경로다

3.4 어드미션 컨트롤 — 내용물을 보는 마지막 검문

세 관문이 보는 것

  • 세 번째 관문은 앞의 둘과 보는 것이 다르다
관문묻는 것YAML
인증“이 요청 보낸 사람이 누구지?”안 봄
인가“그 사람이 pods 를 create 할 자격이 있나?”안 봄
어드미션“그래서 그 YAML 에 뭐라고 적었는데?”연다

어드미션이 잡는 요청: 권한은 있는데 내용이 문제

  • 예: 파드를 만들 권한은 분명히 있는 개발자가 검증 안 된 외부 이미지를 쓰려 한다
  • 인가는 통과시키고 어드미션이 막는다

두 종류와 순서

  • 두 종류가 있고 순서가 있다
  • 먼저 다 고쳐놓고 최종 결과물을 검사해야 말이 되니 이 순서다
인가를 통과한 요청1단계Mutating(변형) — 요청 내용을 고친다안 쓴 것을 채워 넣음· PVC 에 기본 StorageClass 자동 지정· 서비스 메시 사이드카 자동 주입 (24장 4.1)· ServiceAccount 토큰 볼륨 자동 추가 (4.3)2단계Validating(검증) — 보기만 하고 통과/거부만 정한다· 사내 레지스트리 이미지가 아니면 거부· 리소스 limit 없으면 거부
먼저 다 고쳐놓고, 최종 결과물을 검사한다

웹훅으로 붙이는 규칙

  • 내장 컨트롤러 외에 웹훅으로 직접 규칙을 붙일 수 있다
  • ValidatingWebhookConfiguration, MutatingWebhookConfiguration이라는 전용 리소스로 등록한다
  • 여기서 ABAC와의 대비가 다시 나온다
    • 파일 수정 후 재시작이 필요했던 ABAC와 달리 이건 리소스라 kubectl apply로 즉시 반영된다
    • 문서 제목이 Dynamic Admission Control인 이유다

조직 정책이 실제로 강제되는 지점

  • “모든 컨테이너는 limit을 명시해야 한다"는 규칙이 문서에만 있으면 아무도 안 지킨다
  • 어드미션 컨트롤러에 넣으면 정문에서 자동 집행된다
  • 요즘은 코드를 직접 짜기보다 OPA Gatekeeper나 Kyverno로 정책만 선언한다

함정: 웹훅 서버가 죽으면

  • failurePolicy가 그걸 정한다
failurePolicy웹훅이 죽었을 때
Fail보안은 확실하지만, 웹훅이 죽는 순간 아무것도 배포할 수 없다
Ignore클러스터는 돌지만, 그동안 정책 검사가 통째로 건너뛰어진다

두 선택지의 사고

  • 둘 다 나름의 사고를 부른다
  • 24장 3.2에서 “CNI가 지원 안 하면 조용히 아무것도 안 막힌다"고 했던 것과 같은 종류의 위험
    • 막고 있다고 믿는데 실제로는 안 막히는 상태다

4. Subject — 누구인가

주체: 요청에 결부된 신원

  • 주체(subject)는 요청에 결부된 신원이다
  • 사람 그 자체가 아니라 요청에 붙어 있는 정보라는 점이 중요하다
    • 같은 사람이라도 노트북에서 kubectl을 치면 사람 주체
    • CI 파이프라인에서 스크립트를 돌리면 서비스 어카운트 주체
  • 요청 단위로 판단한다
사람 사용자클러스터API 서버노드파드ServiceAccountkubectl
주체는 둘 — 밖에서 오는 사람, 안에서 오는 파드. API 서버 입장에선 둘 다 그저 요청이다

주체 세 종류

  • 종류는 셋이고, 성격이 극단적으로 갈린다
UserGroupServiceAccount
쿠버네티스 리소스❌❌✅
실체문자열문자열 목록etcd에 저장된 객체
kubectl get불가불가kubectl get sa
오타 검증안 됨안 됨됨
관리 주체외부 계정 시스템외부 계정 시스템개발자

4.1 User — 리소스가 아니라 문자열이다

사용자 객체가 없다

  • 쿠버네티스 문서가 직접 못 박는다 — 일반 사용자 계정을 나타내는 객체가 없다
  • 전부 없는 명령이다
    • kubectl get users
    • kubectl create user hong
    • kubectl delete user hong
  • 쿠버네티스는 자기 클러스터에 누가 접근할 수 있는지 목록조차 갖고 있지 않다
    • 물어봐도 대답을 못 한다

사용자의 실체: 인증 결과의 문자열

  • 인증 결과로 나온 문자열이 전부다
    • OIDC 토큰의 email이거나 인증서의 CN이거나
  • 그 문자열의 주인이 실존하는지, 퇴사했는지, 심지어 오타인지도 모른다
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
파드코드가 도는 그릇담겨서 실행됨오퍼레이터실제로 일하는 코드주체는 여기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의 “기본값이 위험한 쪽"과 같은 구조이고, 답도 같다 — 기본을 뒤집는다
이 파드가 쿠버네티스 API 를 호출하나?아니오예nginx, 프론트엔드, 배치 작업 등 대부분→ automount: false오퍼레이터, 컨트롤러→ 전용 SA 를 만들어 지정
기준은 하나 — 쿠버네티스 API를 호출하나

신분증을 안 주는 것이 더 강하다

  • 권한을 좁히는 것보다 신분증 자체를 안 주는 것이 더 강한 조치다
  • 없는 것은 훔칠 수도 없다

SA의 다른 두 필드

  • SA에는 신원 외에 두 필드가 더 있다
필드용도방향
imagePullSecrets비공개 레지스트리 로그인 정보kubelet이 이미지 받아올 때
secrets + enforce-mountable-secrets 어노테이션마운트 가능한 Secret 화이트리스트파드가 볼륨 붙일 때

imagePullSecrets: 편의 기능

  • 파드마다 쓰는 대신 SA에 한 번 붙이면 자동 주입된다
    • 그 주입도 어드미션 컨트롤러가 한다
  • default SA에 붙여두는 게 흔한 패턴이다

마운트 가능한 Secret 목록: 보안 기능

  • 막으려는 구멍이 흥미롭다
  • 파드는 기본적으로 자기 네임스페이스의 아무 Secret이나 마운트할 수 있고, 이건 RBAC로 막히지 않는다
  • kubelet이 대신 읽어서 파일로 넣어주는 구조다
    • 파드의 SA에 Secret 읽기 권한이 없어도 마운트는 된다
    • RBAC를 우회하는 경로가 하나 있다는 뜻이다
같은 네임스페이스RBAC 를 우회하는 경로Secret네임스페이스의 아무 Secretkubelet대신 읽음파드ServiceAccountSecret 읽기 권한 없음파일로 넣어줌 → 마운트는 된다SA 로 직접 읽기: 권한 없음
kubelet 이 대신 읽으니 RBAC 로 막히지 않는다

실무에서 드문 이유

  • 어노테이션 스위치와 목록을 둘 다 켜야 작동한다(하나만 있으면 무효)
  • 요즘은 Kyverno 같은 정책 도구로 처리하는 편이라 실무에서 자주 보이진 않는다

4.3 토큰은 어떻게 들어오나 — Secret 에서 projected 로

SA 지정 이후는 전부 자동

  • 파드에 SA를 지정하면 그다음은 전부 자동이다
ServiceAccount 생성쿠버네티스가 JWT 발급·서명·갱신·무효화까지 전담파드가 그 SA 를 지정 (또는 default)어드미션 컨트롤러가 볼륨 설정을 자동으로 끼워 넣음파드 안에 파일로 등장/var/run/secrets/kubernetes.io/serviceaccount/token
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파드여기가 취약점지금발급파드중간 표현이 사라졌다
중간의 Secret 이 사라졌다

오해 주의: 노출 차단이 아니라 경로 축소

  • 이건 토큰 노출을 막는 게 아니라 경로를 줄인 것이다
  • 여전히 남아 있는 경로가 있다
경로Secret 방식projected
파드 침입가능가능
kubectl exec가능가능 ← pods/exec 권한은 사실상 토큰 읽기 권한이다
노드 침입가능가능
Secret 읽기가능불가 ← 사라진 것
etcd / 백업가능불가 ← 사라진 것

개선의 의미: 시한부 토큰

  • 사라진 둘이 가장 조용하고 넓은 경로였다
  • 여기에 만료와 파드 종속이 겹치니, 어느 경로로 훔쳐도 시한부가 된다
    • 단, 기본 설정에선 만료가 최대 1년까지 늘어나니 실제로 수명을 끊는 건 파드 종속 쪽이다

실전 함정: 토큰 파일 캐시

  • 자동 교체되므로 프로그램이 토큰 파일을 한 번만 읽고 캐시하면 안 된다
  • 증상: 만료 연장을 끈 클러스터에서 잘 돌던 앱이 몇 시간 뒤 401을 뱉는다
  • 공식 클라이언트 라이브러리는 알아서 다시 읽는다

만료 없는 토큰이 필요할 때

  • 예: 클러스터 밖 CI 도구
  • Secret을 수동으로 만들 수는 있지만, 옛 방식의 단점을 그대로 떠안는다
  • 요즘 권장은 kubectl create token my-sa --duration=1h
    • 수명을 지정해 받고 저장하지 않는다

4.4 Group — 명단은 클러스터 밖에 있다

사람 쪽을 묶는 그룹

  • Role이 권한 쪽을 묶는다면, 그룹은 사람 쪽을 묶는다
  • 양쪽에서 묶어야 관리가 완성된다
그룹 없이그룹으로Role 하나alicebob…… 50 번 반복, 입퇴사 때마다 수정Role 하나"developers" 그룹한 번이면 끝
Role 이 권한을 묶듯, 그룹은 사람을 묶는다

역할로 부르기

  • 24장 3.4의 “역할 출입증” 라벨링과 발상이 정확히 같다
  • 이름을 직접 부르지 않고 역할로 부른다

그룹 멤버십 구조의 부재

  • 그런데 쿠버네티스에 그룹 멤버십 구조가 없다
- kind: Group
  name: developers
  # members: [...]   ← 이런 필드는 없다

명단을 가진 쪽: 인증 시스템

  • developers에 누가 속하는지 쿠버네티스는 모른다
  • 그 판단은 인증 시스템이 이미 끝냈고, 결론만 토큰에 실려 온다
{ "email": "alice@company.com", "groups": ["developers", "oncall"] }

RBAC가 하는 일: 문자열 대조

  • “이 사용자가 그 그룹에 속하나?“를 검사하는 게 아니다
  • 요청에 딸려온 그룹 문자열 목록에 그 이름이 들어 있는지 대조할 뿐이다

콘서트장 입구 비유

  • 직원은 VIP 명단을 갖고 있지 않다
    • 팔찌에 “VIP” 라고 적혀 있는지만 본다
  • VIP 가 몇 명인지, 누구인지 모른다
  • 명단은 티켓 판매처에 있다

따라 나오는 두 가지

  1. 소속을 바꿔도 즉시 반영되지 않는다
    • 회사 계정에서 빼도 이미 발급된 토큰에는 그룹이 박혀 있다 → 만료까지 유효
  2. 토큰을 위조할 수 있으면 아무 그룹이나 될 수 있다
    • 서명 검증이 신뢰의 유일한 뿌리다

서비스 어카운트 그룹: 이름에서 계산

  • 서비스 어카운트 그룹은 예외적으로 규칙이 있다
  • 명단이 있어서가 아니라 이름에서 계산되기 때문이다
system:serviceaccount:dev:my-operator네임스페이스잘라내면system:serviceaccounts:devsystem:serviceaccounts지정할 필요 없이 자동으로 붙는다
SA 그룹은 명단이 아니라 이름에서 계산된다

SA 그룹의 범위 주의

  • 지정할 필요 없이 자동으로 붙는다
  • “dev의 모든 파드에게"를 한 줄로 쓸 수 있다
  • 범위가 넓어 새 파드가 생기면 자동으로 권한을 갖게 되니 조심해야 한다
사람 그룹SA 그룹
어디서 오나회사 계정 시스템이름에서 자동 계산
자유롭게 정의가능불가 (규칙 고정)
멤버 조회불가kubectl get sa -n dev

username이 필요한 이유

  • 권한을 그룹으로 준다면 개인 이름은 무의미해 보인다
  • 두 용도가 남는다
    • 개인에게 직접 주는 경우
    • 감사 로그
  • 프로덕션 파드가 삭제됐는데 로그에 developers만 있고 이름이 없다면 팀원 20명 중 누가 했는지 영영 모른다
필드답하는 것
groups뭘 할 수 있나 (권한)
username누가 했나 (책임)

5. RBAC — 무엇을 할 수 있는가

인가 방식: 사실상 RBAC

  • 인가 방식도 플러그형이라 갈아 끼울 수 있다(RBAC / ABAC / 웹훅 / Node)
  • 하지만 결론은 이미 나 있다 — 거의 모든 클러스터가 RBAC를 쓴다

ABAC가 밀려난 이유

  • 이 책의 다른 장들과도 통한다
ABACRBAC
저장노드의 파일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
appsdeployments, statefulsets, daemonsets
batchjobs, cronjobs
rbac.authorization.k8s.ioroles, rolebindings

그룹을 잘못 적으면: 조용히 무효

  • 그래서 파드와 Deployment 권한은 규칙을 나눠 써야 한다
  • 그룹을 잘못 적으면 에러도 안 나고 apply는 성공하는데 조용히 아무 효력이 없다
  • “권한 줬는데 왜 안 되지?“의 십중팔구가 이것이다
  • kubectl api-resources로 확인하는 습관이 필요하다

verb: HTTP보다 잘게 쪼갠 동작

  • verb는 HTTP 메서드에 대응되지만 더 잘게 쪼개져 있다
verbHTTP주의점
getGET이름을 알아야만 조회
listGET뭐가 있는지 모르는 상태에서 전부 훑는다
watchGET연결을 열어두고 변화 알림 — 오퍼레이터 필수
createPOST
update / patchPUT / PATCH따로 줘야 한다
deleteDELETE
deletecollectionDELETE한 번에 전부 삭제 — 매우 위험

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해도 아무 일도 일어나지 않는다
  • 아직 아무에게도 안 붙었기 때문이다
주체User (문자열)Group (문자열)ServiceAccount (리소스)네임스페이스: defaultRoleBindingsubjects + roleRefRoleapiGroups · resources · verbs"" · pods · get, listpods (default 안)subjectsroleRef허용 범위Role 만 만들면 아무 일도 없다 — RoleBinding 이 선을 그어야 권한이 생긴다
RBAC — 주체와 권한은 서로 모르고, 바인딩이 둘을 잇는다
# 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을 여러 개 만든다
범위: 네임스페이스RoleBinding주체Role"리소스 : verb, verb ..."M : N
주체와 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에서 본 것과 같은 이유로, 권한은 모든 경로에서 합쳐진다
개인 바인딩+developers 그룹+system:authenticated+ClusterRoleBindingalice 의 최종 권한= 전부 합집합, 빼는 방법 없음
권한은 모든 경로에서 합쳐진다

흔한 실수: 개인 바인딩만 지우기

  • 특정 사용자의 권한을 줄이려고 그의 RoleBinding을 지운다
  • 그런데 그룹으로 들어오는 권한이 남아 여전히 가능한 경우가 흔하다

5.3 네 가지 조합 — 범위는 바인딩이 정한다

바인딩과 권한 정의의 네 조합

  • Role은 네임스페이스에 묶인다
  • 그래서 짝이 되는 클러스터 범위 리소스가 따로 있고, 조합이 넷이다
바인딩권한 정의적용 범위
RoleBindingRole그 네임스페이스에만
RoleBindingClusterRole그 네임스페이스에만 (정의만 재사용)
ClusterRoleBindingClusterRole클러스터 전체
ClusterRoleBindingRole❌ 불가능

네 번째가 안 되는 이유

  • 이 구조를 이해하는 열쇠다
  • ClusterRoleBinding은 “모든 네임스페이스에 적용"인데 Role은 한 네임스페이스분의 권한밖에 없다
  • 바인딩이 요구하는 범위를 권한이 채울 수 없다

가장 헷갈리는 것: ClusterRole의 뜻

  • ClusterRole 은 “클러스터 전역 권한"이 아니라 “클러스터 전역에서 참조 가능한 정의"다
  • 범위는 바인딩이 정한다
    • 같은 ClusterRole을 RoleBinding으로 붙이면 그 네임스페이스에만
    • ClusterRoleBinding으로 붙이면 전체에 적용된다

ClusterRole의 두 용도

  1. 네임스페이스가 없는 리소스를 다루려고 — 선택의 여지가 없다
    • nodes, namespaces, CustomResourceDefinitions, persistentvolumes, storageclasses, clusterroles
  2. 여러 네임스페이스에서 재사용하는 템플릿 — 선택의 문제
    • 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은 늘지만 권한은 딱 그 네임스페이스들에만 열린다
  • 새 네임스페이스가 생겨도 자동으로 퍼지지 않는다
  • 그 명시성이 안전장치가 된다
ClusterRole: view-pod네임스페이스 dev-1파드RoleBinding네임스페이스 dev-2RoleBinding파드네임스페이스: testServiceAccount: test-saget, listget, list
ClusterRole 하나를 여러 RoleBinding 이 참조한다 — 정의는 재사용, 범위는 dev-1 과 dev-2 뿐

ClusterRoleBinding으로 바꾸면

  • 이 그림을 ClusterRoleBinding 으로 한 줄 바꾸면 어떻게 되는지가 다음이다
네임스페이스 dev-1파드네임스페이스 dev-2파드네임스페이스 ...파드네임스페이스 dev-N파드······네임스페이스: testServiceAccount: test-saClusterRole: view-podClusterRoleBindingget, listget, listget, listget, list바인딩 하나로 회색 네임스페이스 — 아직 만들지도 않은 것까지 — 전부 열린다
ClusterRoleBinding — 같은 ClusterRole 인데 범위가 클러스터 전체로 바뀐다

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: []는 “규칙이 없다"가 아니라 “내가 안 쓸 테니 네가 채워라”**다
  • 실제로 조회하면 수십 줄이 들어 있다
  • 여기에 수동으로 뭘 적어도 컨트롤러가 곧 덮어쓴다
  • default SA를 지워도 다시 생기는 것과 같은, “쿠버네티스가 소유한 필드"다

자동으로 쌓이는 계단

  • 라벨과 셀렉터가 양쪽에 다 있어서 계단이 자동으로 쌓인다
개별ClusterRole 들aggregate-to-view라벨viewaggregate-to-edit라벨을 갖고 있음editaggregate-to-admin라벨을 갖고 있음admin모여서맨 아래 하나만 추가해도 위로 전부 전파
라벨과 셀렉터로 계단이 자동으로 쌓인다

맨 아래 하나로 전파

  • 누가 “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 사용자들바인딩 = 파이프 (누구 → view)관리자가 예전에 이미 놓았다view = 물통권한 내용postgres-vieweraggregate-to-view 라벨여기에 물을 더 붓는 것
파이프는 그대로 두고 물통을 채운다

파이프는 그대로, 물통만 채운다

  • 파이프는 그대로 두고 물통을 채우니, 기존 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를 채워 돌려준다
  • 평소 쓰는 판단 로직을 그대로 돌리되 실행만 안 하는 것이라 결과가 정확하다
  • 저장은 되지 않는다 — 리소스 형식을 빌린 함수 호출에 가깝다
kubectl auth can-i--as 로 남인 척 가능API 서버SubjectAccessReview 리소스 생성리소스 형식을 빌린 함수 호출인가 컨트롤러가 status 를 채움평소 판단 로직 그대로 — 실행만 안 함저장은 되지 않는다질문yes
질문조차 리소스로 만들어 판단 로직에 넣는다

한계: 이유는 알려주지 않는다

  • 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 세 가지 조언 — 필요 없는 건 주지 마라

책의 세 조언: 전부 같은 말의 변주

  1. 와일드카드를 피하라
    • verbs: ["*"] 는 deletecollection 과 pods/exec 까지 포함하고, 미래 리소스도 포함한다
    • → “와일드카드 금지"를 정책으로 세우고, 타당한 이유가 있을 때만 예외로 푼다
      • 그때그때 판단하면 결국 편한 쪽으로 흐른다
  2. 파드에 cluster-admin 을 주지 마라. 절대로
    • 책에서 “Never.” 를 문장 하나로 따로 쓴 유일한 대목
    • 권한 수정 가능 → 스스로 무한 확장
    • 모든 Secret 열람 → 클러스터 밖까지
    • 사람 관리자와 달리 파드는 코드라 취약점이 있을 수 있다
  3. 토큰을 자동 마운트하지 마라
    • automountServiceAccountToken: false
    • 기준은 하나 — 이 파드가 쿠버네티스 API 를 호출하나?

세 조언의 공통점: 이 패턴의 요약

조언뜻
와일드카드필요 없는 권한을 주지 마라
cluster-admin필요 없는 최고 권한을 주지 마라
토큰 마운트필요 없는 신분증을 주지 마라

효과 순서: ② → ③ → ①

  • ②는 하나만 지켜도 최악을 피한다
  • ③은 대부분의 파드에 즉시 적용 가능하다
  • ①은 꾸준히 지켜야 하는 습관이다

조합 선택: 좁은 것에서 시작

  • 조합 선택은 좁은 것에서 시작해 필요할 때만 넓히는 순서다
Role + RoleBinding여기서 출발여러 네임스페이스가 필요하면ClusterRole + RoleBinding (여러 개)전역 리소스(nodes, CRD)가 정말 필요하면ClusterRole + ClusterRoleBinding마지막 수단
좁은 것에서 시작해 필요할 때만 넓힌다

6.2 24장과의 구분 — 조작 권한 vs 통신 권한

같은 클러스터의 두 상황

  • 24장 5.1에서 이미 짚었지만, 26장을 읽고 나면 경계가 더 선명해진다
상황 1 — 개발자 민수가 노트북에서26장kubectl delete deployment payment -n chili-shop민수노트북요청이 "API 서버"로 간다API 서버세 관문 통과 여부를 RBAC 가 판단상황 2 — frontend 파드가 앱 코드로24장DELETE http://backend:8080/orders/123frontend 파드NetworkPolicy (IP·포트)+ AuthorizationPolicy (경로·신원)backend 파드쿠버네티스는 이 요청의 존재조차 모른다 — 파드→파드 HTTP 일 뿐
API 서버로 가면 26장, 파드로 가면 24장
RBAC (26장)NetworkPolicy / AuthorizationPolicy (24장)
요청 목적지API 서버내 앱 파드
무엇을 통제쿠버네티스 리소스 조작파드 간 통신
비유건물 관리 권한 — 방을 만들고 부수는 것방끼리 왕래 규칙
신원의 근거토큰의 username·groupsIP(NetworkPolicy) / 인증서(mTLS)

둘은 서로 만나지 않는다

  • 민수에게 RBAC 전권을 줘도 그가 frontend 파드 안에서 DELETE /orders를 날리면 AuthorizationPolicy가 막는다
  • AuthorizationPolicy가 아무리 빡빡해도 kubectl delete pod는 RBAC 영역이다

만나는 한 곳: ServiceAccount

  • 흥미로운 건 한 곳에서 만난다는 점이다
체계ServiceAccount의 쓰임
RBACSA 에 Role 을 붙여 “쿠버네티스에 뭘 할 수 있나"를 정한다
Istio mTLSSA 이름이 그대로 신원이 되어 “누구와 통신할 수 있나"의 근거가 된다 — cluster.local/ns/chili-shop/sa/frontend

같은 신분증, 다른 문

  • 같은 신분증이 두 체계에서 각각 다른 문을 여는 셈이다
  • 24장 4.2에서 “ServiceAccount는 파드가 쓰는 계정"이라고 짧게 넘어갔던 것의 정체가 26장 전체였다

6.3 모든 것이 리소스다 — 질문조차

관통하는 설계 철학

  • 쿠버네티스는 모든 것을 API 리소스로 표현하려 한다
대상리소스비고
권한Role, RoleBindingABAC 의 파일과 대비
신분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

사람 사용자가 쿠버네티스 리소스가 아니라는 사실은 공식 문서 "Authenticating"에 그대로 적혀 있다.
"Kubernetes does not have objects which represent normal user accounts."
[해석] "쿠버네티스에는 일반 사용자 계정을 나타내는 객체가 없다." — 4.1의 근거이자 이 장 전체의 비대칭(사람은 밖, 파드는 안)을 만드는 한 문장이다. 같은 문서는 인증 플러그인을 여러 개 켰을 때 하나만 성공하면 통과한다는 점과 평가 순서가 보장되지 않는다는 점(3.2), 인증 결과가 username·UID·groups·extra 네 필드로 표현된다는 점(3.3)을 설명한다.
RBAC 레퍼런스는 rules의 세 축(5.1), 규칙 간 OR 평가와 거부 규칙의 부재, Role/ClusterRole × RoleBinding/ClusterRoleBinding 네 조합 중 ClusterRoleBinding + Role이 불가능하다는 점(5.3), 그리고 aggregationRule을 설정하면 rules가 컨트롤러 소유가 된다는 점(5.4)을 정의한다. 기본 제공 ClusterRole view/edit/admin의 정확한 권한 범위도 여기 있다.
서비스 어카운트 토큰이 Secret에서 projected 볼륨으로 전환된 배경(4.3)은 Bound Service Account Tokens 관련 문서와 1.24 릴리스 노트에 정리돼 있다. 만료·대상(audience)·파드 종속이 한꺼번에 들어온 변화다.
책 예제에서 눈에 띈 것 — Problem 절의 "authentication has two fundamental parts and authorization"은 어순이 깨진 문장으로 보이며, 문맥상 "접근 제어는 인증과 인가 두 부분으로 이루어진다"는 뜻이다. Table 26-1과 26-6은 PDF 추출 과정에서 열이 섞여 원문 대조가 필요하다.
→ kubernetes.io — Authenticating
→ kubernetes.io — Using RBAC Authorization
→ kubernetes.io — Admission Controllers
→ kubernetes.io — Managing Service Accounts
→ GitHub — rakkess (kubectl access-matrix)
→ GitHub — KubiScan