LabHub
배우기 러닝패스 코스

CKAD — 쿠버네티스 애플리케이션 개발자 · 내 요청은 어디서 검사받는가 — 인증, 인가, 어드미션, API 폐기 · 이론

토큰, 역할, 어드미션, 그리고 사라지는 API 버전

LabHub 에서 이어서 보기

한 줄 요약

파드가 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 폐기 정책](https://kubernetes.io/docs/reference/using-api/deprecation-policy/) 이 예고한 일정입니다.

어떻게 동작하나

인증 — 서비스 어카운트 토큰은 어떻게 검사되나

[서비스 어카운트](https://kubernetes.io/docs/concepts/security/service-accounts/) 문서에 따르면 서비스 어카운트는 서명된 JWT 로 API 서버에 인증합니다. 클라이언트는 Authorization: Bearer <token> 헤더를 붙이고, API 서버는 이 순서로 검사합니다 — 토큰 서명, 만료 여부, 토큰 클레임이 가리키는 오브젝트 참조가 지금도 유효한지, 토큰이 지금 유효한 시점인지, 그리고 audience 클레임. TokenRequest API 로 발급된 토큰은 파드 같은 클라이언트의 수명에 바인딩되어 있어서, API 서버는 그 오브젝트가 고유 ID 그대로 아직 존재하는지도 확인합니다. Secret 에 담긴 옛 방식 토큰은 Secret 과 대조합니다.

서명 키는 [서비스 어카운트 관리](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) 문서가 설명합니다. 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 기반 토큰이었고 이 메커니즘이 그것을 대체했습니다. 토큰이 필요 없는 파드는 [서비스 어카운트 설정](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 문서대로 ServiceAccount 나 파드 스펙에 automountServiceAccountToken: false 를 두어 마운트를 끕니다(둘 다 있으면 파드 스펙이 이깁니다). 외부 시스템용으로는 같은 문서의 예처럼 audience: vault, expirationSeconds: 7200 을 지정한 projected 토큰을 쓰고, kubelet 은 TTL 의 80% 가 지나거나 24시간이 넘으면 교체를 요청하므로 애플리케이션은 파일을 주기적으로 다시 읽어야 합니다.

인가 — RBAC 는 더하기만 한다

[RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) 문서의 핵심 문장은 "권한은 순수하게 더해지며 거부 규칙은 없다" 입니다. Role 은 네임스페이스 안의 권한이고 ClusterRole 은 네임스페이스에 속하지 않는 리소스라 클러스터 범위 리소스에 대한 권한이나 여러 네임스페이스에서 재사용할 권한 묶음을 정의합니다. RoleBinding 은 주체(사용자·그룹·서비스 어카운트)에 Role 이나 ClusterRole 을 특정 네임스페이스 안에서 붙이고, ClusterRoleBinding 은 클러스터 전체에 붙입니다.

[RBAC 모범 사례](https://kubernetes.io/docs/concepts/security/rbac-good-practices/) 가 말하는 최소 권한은 구체적입니다 — 가능하면 네임스페이스 수준에서 권한을 주고, ClusterRoleBinding 대신 RoleBinding 을 써서 특정 네임스페이스 안으로 한정하며, 와일드카드를 피하고(지금 있는 리소스뿐 아니라 앞으로 생길 모든 리소스 타입에 권한을 주는 셈이므로), cluster-admin 은 꼭 필요할 때만 씁니다. 개발자에게 이것은 "내 파드의 서비스 어카운트에 ConfigMap 읽기만 필요하면 그 네임스페이스에 get·list 만 가진 Role 과 RoleBinding 을 만든다" 로 옮겨집니다.

어드미션 — 스펙은 여기서 바뀌거나 거부된다

[어드미션 컨트롤러](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) 문서에 따르면 어드미션 컨트롤러는 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](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/) 는 외부 호출 없이 CEL(Common Expression Language)로 검증 규칙을 API 안에 선언하는 방식이며, MutatingAdmissionPolicy 는 같은 방식의 변경입니다. 그래서 kubectl get pod -o yaml 로 본 파드가 내가 낸 YAML 과 다르다면 — 토큰 볼륨이 생겼거나, 요청량이 채워졌거나, 사이드카가 붙었거나 — 누군가 고친 것이 아니라 mutating 단계가 한 일입니다. 어느 어드미션이 거부했는지는 오류 메시지에 컨트롤러 이름이 나옵니다.

API 폐기 — 정해진 규칙으로 사라진다

[API 폐기 정책](https://kubernetes.io/docs/reference/using-api/deprecation-policy/) 은 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 이전 안내](https://kubernetes.io/docs/reference/using-api/deprecation-guide/) 는 릴리스마다 서비스가 중단된 버전을 나열합니다. 예를 들어 v1.32 는 flowcontrol.apiserver.k8s.io/v1beta3 의 FlowSchema·PriorityLevelConfiguration 을 더는 서비스하지 않으며 v1.29 부터 있던 v1 으로 옮겨야 합니다. 옮기는 절차도 같은 문서에 있습니다.

1. 찾기. 1.19 이상에서는 클라이언트 경고, 메트릭, 감사 정보로 폐기된 API 사용처를 찾습니다. 지금 서버가 무엇을 서비스하는지는 [kubectl api-resources](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_api-resources/) 로 봅니다 — -o wide 로 지원 verb 까지, --api-group=<그룹> 으로 특정 그룹만, --namespaced=false 로 클러스터 범위 리소스만 볼 수 있습니다.
2. 시험하기. API 서버에 --runtime-config=<그룹>/<버전>=false 를 주어 곧 제거될 버전을 미리 꺼 보고 무엇이 깨지는지 확인합니다.
3. 옮기기. 컨트롤러와 통합 코드는 폐기되지 않은 API 를 부르도록 고치고, YAML 은 kubectl convert -f <파일> --output-version <그룹>/<버전> 으로 변환합니다. 예를 들어 옛 Deployment 는 --output-version apps/v1 입니다. 변환은 이상적이지 않은 기본값을 넣을 수 있으니 결과를 API 참조와 대조합니다. kubectl convert 는 한때 kubectl 에 내장되어 있었지만 지금은 기본 설치에 없는 플러그인이라 [설치 문서](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) 의 절차로 따로 받습니다.

현장에서 만나는 모습

RBAC 를 열었는데 계속 401 이 난다. 401 은 인증 실패입니다. 인가(RBAC)는 인증을 통과한 뒤의 관문이라 Role 을 아무리 넓혀도 401 은 바뀌지 않습니다. 파드가 마운트한 토큰이 만료됐는데 애플리케이션이 시작할 때 한 번만 읽고 다시 읽지 않는 경우가 전형적입니다 — 문서가 주기적으로 다시 읽으라고 하는 이유입니다.

PVC 에 storageClassName 을 안 적었는데 어떤 클래스가 붙어 있다. DefaultStorageClass 어드미션이 넣은 것입니다. 기본 StorageClass 가 없으면 아무 일도 하지 않고, 둘 이상이 기본으로 표시되어 있으면 문서가 정한 별도 규칙을 따릅니다. "내가 안 적은 값이 들어 있다" 는 대부분 mutating 어드미션의 흔적입니다.

다음 퀴즈에서 확인할 것

퀴즈에서는 서비스 어카운트 토큰 검사 항목과 기본 수명, 서명 키가 있는 곳, RBAC 의 더하기 성질과 바인딩 범위, 어드미션의 두 단계와 읽기 요청, beta API 의 수명 규칙, 그리고 kubectl convert 의 쓰임을 묻습니다.