令牌、角色、准入,以及会消失的 API 版本
한국어 원문으로 표시합니다.
한 줄 요약
파드가 API 서버에 보내는 요청은 인증 → 인가 → 어드미션 세 관문을 차례로 지납니다. 서비스 어카운트 토큰은 서명·만료·바인딩된 오브젝트·대상(audience)을 검사받고, RBAC 는 더하기만 가능한 규칙으로 허용 범위를 정하며, 어드미션 컨트롤러는 스펙을 고치거나(mutating) 거부합니다(validating). 그리고 API 버전은 정해진 규칙에 따라 사라지므로 kubectl api-resources 로 지금 서버가 무엇을 서비스하는지 보고 kubectl convert 로 매니페스트를 옮깁니다.
왜 이게 필요했나
개발자가 만나는 증상은 늘 같은 세 가지입니다. "401 Unauthorized", "forbidden", 그리고 "내가 쓴 YAML 과 돌아가는 파드가 다르다". 첫째는 인증, 둘째는 인가, 셋째는 어드미션의 결과인데, 셋을 구분하지 못하면 RBAC 를 열어도 401 이 계속 나거나, 토큰을 갈아도 forbidden 이 그대로입니다. 네 번째 증상은 업그레이드 뒤에 옵니다 — 어제까지 되던 kubectl apply 가 "no matches for kind" 로 실패합니다. 서버가 그 API 버전을 더는 서비스하지 않기 때문이고, 이것은 사고가 아니라 API 폐기 정책 이 예고한 일정입니다.
어떻게 동작하나
인증 — 서비스 어카운트 토큰은 어떻게 검사되나
서비스 어카운트 문서에 따르면 서비스 어카운트는 서명된 JWT 로 API 서버에 인증합니다. 클라이언트는 Authorization: Bearer <token> 헤더를 붙이고, API 서버는 이 순서로 검사합니다 — 토큰 서명, 만료 여부, 토큰 클레임이 가리키는 오브젝트 참조가 지금도 유효한지, 토큰이 지금 유효한 시점인지, 그리고 audience 클레임. TokenRequest API 로 발급된 토큰은 파드 같은 클라이언트의 수명에 바인딩되어 있어서, API 서버는 그 오브젝트가 고유 ID 그대로 아직 존재하는지도 확인합니다. Secret 에 담긴 옛 방식 토큰은 Secret 과 대조합니다.
서명 키는 서비스 어카운트 관리 문서가 설명합니다. kube-controller-manager 의 토큰 컨트롤러가 --service-account-private-key-file 의 개인 키로 토큰을 서명하고, kube-apiserver 는 --service-account-key-file 의 공개 키로 검증합니다. 파드에 토큰이 들어가는 길은 ServiceAccount 어드미션 컨트롤러가 붙이는 projected 볼륨입니다(1.22 부터 안정, 끌 수 없음).
- name: kube-api-access-<random-suffix>
projected:
sources:
- serviceAccountToken:
path: token
- configMap:
name: kube-root-ca.crt
items: [{key: ca.crt, path: ca.crt}]
- downwardAPI:
items: [{fieldRef: {fieldPath: metadata.namespace}, path: namespace}]
세 소스 중 첫째가 핵심입니다. kubelet 이 TokenRequest API 로 시간 제한이 있는 토큰을 받아 넣고(기본 수명 1시간), 만료 전에 갱신하며, 토큰은 그 파드에 바인딩되고 kube-apiserver 를 audience 로 갖습니다. 옛 방식은 만료되지 않는 Secret 기반 토큰이었고 이 메커니즘이 그것을 대체했습니다. 토큰이 필요 없는 파드는 서비스 어카운트 설정 문서대로 ServiceAccount 나 파드 스펙에 automountServiceAccountToken: false 를 두어 마운트를 끕니다(둘 다 있으면 파드 스펙이 이깁니다). 외부 시스템용으로는 같은 문서의 예처럼 audience: vault, expirationSeconds: 7200 을 지정한 projected 토큰을 쓰고, kubelet 은 TTL 의 80% 가 지나거나 24시간이 넘으면 교체를 요청하므로 애플리케이션은 파일을 주기적으로 다시 읽어야 합니다.
인가 — RBAC 는 더하기만 한다
RBAC 문서의 핵심 문장은 "권한은 순수하게 더해지며 거부 규칙은 없다" 입니다. Role 은 네임스페이스 안의 권한이고 ClusterRole 은 네임스페이스에 속하지 않는 리소스라 클러스터 범위 리소스에 대한 권한이나 여러 네임스페이스에서 재사용할 권한 묶음을 정의합니다. RoleBinding 은 주체(사용자·그룹·서비스 어카운트)에 Role 이나 ClusterRole 을 특정 네임스페이스 안에서 붙이고, ClusterRoleBinding 은 클러스터 전체에 붙입니다.
RBAC 모범 사례 가 말하는 최소 권한은 구체적입니다 — 가능하면 네임스페이스 수준에서 권한을 주고, ClusterRoleBinding 대신 RoleBinding 을 써서 특정 네임스페이스 안으로 한정하며, 와일드카드를 피하고(지금 있는 리소스뿐 아니라 앞으로 생길 모든 리소스 타입에 권한을 주는 셈이므로), cluster-admin 은 꼭 필요할 때만 씁니다. 개발자에게 이것은 "내 파드의 서비스 어카운트에 ConfigMap 읽기만 필요하면 그 네임스페이스에 get·list 만 가진 Role 과 RoleBinding 을 만든다" 로 옮겨집니다.
어드미션 — 스펙은 여기서 바뀌거나 거부된다
어드미션 컨트롤러 문서에 따르면 어드미션 컨트롤러는 kube-apiserver 안의 코드로, 오브젝트를 만들고·지우고·바꾸는 요청의 데이터를 검사합니다. 읽기 요청(get·list·watch)은 어드미션을 거치지 않습니다. 두 단계로 돌아갑니다 — 먼저 mutating 컨트롤러들이 데이터를 바꾸고, 그다음 validating 컨트롤러들이 검사하며, 어느 단계에서든 하나라도 거부하면 요청 전체가 거부됩니다. 1.37 의 기본 활성 목록에는 CertificateApproval, DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds, LimitRanger, MutatingAdmissionPolicy, MutatingAdmissionWebhook, NamespaceLifecycle, PersistentVolumeClaimResize, PodSecurity, Priority, ResourceQuota, RuntimeClass, ServiceAccount, StorageObjectInUseProtection, TaintNodesByCondition, ValidatingAdmissionPolicy, ValidatingAdmissionWebhook 등이 있습니다.
개발자가 매일 겪는 것만 추리면 이렇습니다.
| 컨트롤러 | 종류 | 내 스펙에 하는 일 |
|---|---|---|
| ServiceAccount | mutating + validating | 서비스 어카운트 토큰 볼륨을 붙인다 |
| LimitRanger | mutating + validating | 네임스페이스의 LimitRange 를 어기면 거부하고, 요청량을 안 적으면 기본값을 넣는다 |
| DefaultStorageClass | mutating | storageClassName 이 없는 PVC 에 기본 StorageClass 를 넣는다 |
| NamespaceLifecycle | validating | 종료 중이거나 없는 네임스페이스에 오브젝트를 만들면 거부한다 |
| PodSecurity | validating | 네임스페이스의 Pod Security 라벨에 어긋나는 파드를 거부한다 |
| ResourceQuota | validating | 네임스페이스 쿼터를 넘으면 거부한다 |
네 개의 확장점도 있습니다. MutatingAdmissionWebhook 과 ValidatingAdmissionWebhook 은 API 로 등록한 외부 webhook 을 부르고, ValidatingAdmissionPolicy 는 외부 호출 없이 CEL(Common Expression Language)로 검증 규칙을 API 안에 선언하는 방식이며, MutatingAdmissionPolicy 는 같은 방식의 변경입니다. 그래서 kubectl get pod -o yaml 로 본 파드가 내가 낸 YAML 과 다르다면 — 토큰 볼륨이 생겼거나, 요청량이 채워졌거나, 사이드카가 붙었거나 — 누군가 고친 것이 아니라 mutating 단계가 한 일입니다. 어느 어드미션이 거부했는지는 오류 메시지에 컨트롤러 이름이 나옵니다.
API 폐기 — 정해진 규칙으로 사라진다
API 폐기 정책 은 API 그룹마다 독립적으로 버전이 붙고, 버전은 alpha(v1alpha1)·beta(v1beta1)·GA(v1) 세 트랙을 따른다고 적습니다. 규칙 1: API 요소는 API 그룹의 버전을 올리는 방식으로만 제거할 수 있고, 한 번 특정 버전에 들어간 요소는 그 버전에서 빠지거나 크게 바뀌지 않습니다. 규칙 2: 한 릴리스 안에서 오브젝트는 버전 사이를 정보 손실 없이 왕복할 수 있어야 합니다. 규칙 3: 덜 안정적인 버전을 위해 폐기될 수 없습니다(GA 가 beta 를 대체할 수는 있어도 beta 가 GA 를 대체할 수는 없습니다). 규칙 4a: 수명은 안정성 수준이 정합니다 — GA 버전은 폐기 표시가 될 수는 있어도 쿠버네티스 메이저 버전 안에서는 제거되지 않고, beta 버전은 도입 뒤 9개월 또는 3 minor 중 긴 쪽 안에 폐기되고 폐기 뒤 9개월 또는 3 minor 중 긴 쪽 뒤에 서비스가 중단되며, alpha 버전은 예고 없이 어느 릴리스에서든 제거될 수 있습니다.
폐기 API 이전 안내 는 릴리스마다 서비스가 중단된 버전을 나열합니다. 예를 들어 v1.32 는 flowcontrol.apiserver.k8s.io/v1beta3 의 FlowSchema·PriorityLevelConfiguration 을 더는 서비스하지 않으며 v1.29 부터 있던 v1 으로 옮겨야 합니다. 옮기는 절차도 같은 문서에 있습니다.
- 찾기. 1.19 이상에서는 클라이언트 경고, 메트릭, 감사 정보로 폐기된 API 사용처를 찾습니다. 지금 서버가 무엇을 서비스하는지는 kubectl api-resources 로 봅니다 —
-o wide로 지원 verb 까지,--api-group=<그룹>으로 특정 그룹만,--namespaced=false로 클러스터 범위 리소스만 볼 수 있습니다. - 시험하기. API 서버에
--runtime-config=<그룹>/<버전>=false를 주어 곧 제거될 버전을 미리 꺼 보고 무엇이 깨지는지 확인합니다. - 옮기기. 컨트롤러와 통합 코드는 폐기되지 않은 API 를 부르도록 고치고, YAML 은
kubectl convert -f <파일> --output-version <그룹>/<버전>으로 변환합니다. 예를 들어 옛 Deployment 는--output-version apps/v1입니다. 변환은 이상적이지 않은 기본값을 넣을 수 있으니 결과를 API 참조와 대조합니다.kubectl convert는 한때 kubectl 에 내장되어 있었지만 지금은 기본 설치에 없는 플러그인이라 설치 문서 의 절차로 따로 받습니다.
현장에서 만나는 모습
RBAC 를 열었는데 계속 401 이 난다. 401 은 인증 실패입니다. 인가(RBAC)는 인증을 통과한 뒤의 관문이라 Role 을 아무리 넓혀도 401 은 바뀌지 않습니다. 파드가 마운트한 토큰이 만료됐는데 애플리케이션이 시작할 때 한 번만 읽고 다시 읽지 않는 경우가 전형적입니다 — 문서가 주기적으로 다시 읽으라고 하는 이유입니다.
PVC 에 storageClassName 을 안 적었는데 어떤 클래스가 붙어 있다. DefaultStorageClass 어드미션이 넣은 것입니다. 기본 StorageClass 가 없으면 아무 일도 하지 않고, 둘 이상이 기본으로 표시되어 있으면 문서가 정한 별도 규칙을 따릅니다. "내가 안 적은 값이 들어 있다" 는 대부분 mutating 어드미션의 흔적입니다.
다음 퀴즈에서 확인할 것
퀴즈에서는 서비스 어카운트 토큰 검사 항목과 기본 수명, 서명 키가 있는 곳, RBAC 의 더하기 성질과 바인딩 범위, 어드미션의 두 단계와 읽기 요청, beta API 의 수명 규칙, 그리고 kubectl convert 의 쓰임을 묻습니다.