LabHub
배우기 러닝패스 코스

KCSA — Kubernetesセキュリティアソシエイト

リクエスト一つがapiserverを通る道

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

apiserver 로 들어온 요청은 인증 → 인가 → 어드미션 → 검증 → 저장 순으로 흐릅니다. 각 단계가 무엇을 판단하고 무엇은 판단하지 않는지 알면 대부분의 보안 질문이 풀립니다.

요청 하나가 apiserver 를 통과하는 여섯 단계 — 인증, 인가, Mutating 어드미션, 스키마 검증, Validating 어드미션, etcd 저장. 401 은 1단계 실패, 403 은 2단계 실패이고, 어드미션은 먼저 고치고 나중에 검사해야 앞뒤가 맞기 때문에 Mutating 이 앞이다

フロー図: 인증 → 인가 → 어드미션 → 검증 → 저장・차례로・중요한 사실 하나: 쿠버네티스에는 User 오브젝트가 없습니다.・--anonymous-auth=true

왜 이게 필요했나

"이 요청은 왜 거부됐나"에 답하려면 어느 단계에서 걸렸는지를 알아야 합니다. 401 은 인증 실패, 403 은 인가 실패, 어드미션 웹훅의 거부는 또 다른 메시지입니다. 단계를 모르면 RBAC 을 고쳐야 할 때 인증서를 만지게 됩니다.

어떻게 동작하나

1단계: 인증(Authentication) — "당신은 누구입니까"

apiserver 는 여러 인증기(authenticator)를 차례로 시도하고, 하나라도 성공하면 그 신원으로 진행합니다. 전부 실패하면 요청자는 system:anonymous(그룹 system:unauthenticated)가 됩니다.

방식 쓰이는 곳
X.509 클라이언트 인증서 kubelet, 컨트롤러, 관리자 kubeconfig. CN 이 사용자명, O 가 그룹
ServiceAccount 토큰(JWT) 파드 안에서 API 호출
OIDC 토큰 사람 사용자를 회사 IdP 와 연동
웹훅 토큰 인증 외부 인증 시스템 위임

중요한 사실 하나: 쿠버네티스에는 User 오브젝트가 없습니다. 사용자는 인증기가 만들어 낸 문자열일 뿐이고, 그래서 "사용자를 삭제"할 수 없습니다. 인증서를 폐기하거나 IdP 에서 끊어야 합니다.

--anonymous-auth=true 는 인증 실패를 거부로 만들지 않고 익명 신원을 부여합니다. 익명에게 무엇을 허용할지는 인가 단계가 결정하므로, 익명 접근 자체보다 익명에 붙은 권한이 진짜 문제입니다. 기본 클러스터에서 익명은 /healthz, /version 같은 공개 정보만 볼 수 있습니다.

system:masters 그룹은 특별합니다. 이 그룹의 멤버는 모든 RBAC 검사를 우회하고, RoleBinding 이나 ClusterRoleBinding 을 삭제해도 권한을 회수할 수 없습니다. 인증서에 O=system:masters 가 박히면 그 인증서가 유효한 동안은 사실상 회수 불가능한 최고 권한입니다.

2단계: 인가(Authorization) — "그 일을 해도 됩니까"

인가 모듈도 순서대로 평가되고 하나라도 허용하면 통과합니다. --authorization-mode 플래그로 지정합니다.

RBAC 은 허용만 있고 거부가 없습니다. 어디에도 허용이 없으면 거부입니다. 그래서 "이 사람에게서 권한을 빼앗는 Role" 같은 건 만들 수 없고, 바인딩을 없애야 합니다.

3단계: 어드미션(Admission) — "이 내용이 정책에 맞습니까"

인가를 통과한 요청이라도 내용이 규칙에 맞는지 봅니다. 순서가 중요합니다 — Mutating 이 먼저, Validating 이 나중. 먼저 고치고 그다음 검사해야 앞뒤가 맞습니다.

여기에 사는 대표 플러그인이 PodSecurity(PSA)입니다. 그리고 외부 정책 엔진 (OPA Gatekeeper, Kyverno)도 웹훅으로 이 단계에 끼어듭니다.

어드미션 웹훅에는 운영상 함정이 있습니다. failurePolicyFail 이면 웹훅이 죽었을 때 모든 파드 생성이 거부되고, Ignore 면 정책이 통째로 우회됩니다. 저자의 블로그가 정리한 판단은 분명합니다 — "프로덕션에서는 Fail 을 유지하되 Gatekeeper 파드의 리소스와 replicas 를 충분히 확보한다." 가용성 문제를 정책을 약화시켜 푸는 게 아니라 엔지니어링으로 풀라는 뜻입니다.

4단계: etcd 저장과 저장 시 암호화

apiserver 는 오브젝트를 직렬화(기본 protobuf)한 뒤 transformer 를 거쳐 etcd 에 씁니다. transformer 가 identity 면 평문, aescbc/aesgcm/kms 면 암호문입니다.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [secrets]
    providers:
      - aescbc:
          keys: [{name: key1, secret: <base64>}]
      - identity: {}

providers 배열의 순서가 전부입니다. 첫 번째가 쓰기에 쓰이고, 모든 프로바이더가 읽기에 쓰입니다. 그래서 identity 를 맨 앞에 두면 평문 저장으로 되돌아가고, 맨 뒤에 두면 "새로 쓰는 건 암호화, 예전 평문도 읽기 가능" 이 됩니다. 그리고 설정을 켜도 기존 Secret 은 다시 쓰기 전까지 평문으로 남습니다kubectl get secrets -A -o json | kubectl replace -f - 같은 전체 재작성이 필요합니다.

KMS v2 프로바이더는 한 걸음 더 갑니다. DEK 는 로컬에서 만들고 캐싱하며, 원격 KMS 는 KEK 만 관리합니다(봉투 암호화). 키 순환에 apiserver 재시작이 필요 없습니다.

5단계: kubelet 의 인증/인가

kubelet 은 두 방향의 신원을 갖습니다.

컨트롤러 매니저와 토큰 발급

컨트롤러 매니저는 ServiceAccount 컨트롤러와 토큰 컨트롤러를 돌리고, --service-account-private-key-file 로 받은 키로 SA 토큰에 서명합니다. 이 키를 얻으면 임의의 SA 를 사칭하는 토큰을 직접 발급할 수 있습니다. apiserver 는 대응하는 공개키로 검증만 하니까요.

v1.24 부터는 SA 를 만들어도 영구 토큰 Secret 이 자동 생성되지 않고, TokenRequest API 기반의 시간 제한 토큰이 기본입니다(kubectl create token <sa> --duration=3600s). 보안상 큰 개선입니다. 여기서 --service-account-lookup=false 가 위험한 이유도 나옵니다 — 이 값이 false 면 이미 삭제된 SA 의 토큰도 서명만 유효하면 계속 통과합니다.

현장에서 만나는 모습

저자의 홈랩 확장 기록에 이 체계의 급소가 그대로 나옵니다. 컨트롤 플레인을 추가하려면 CA 개인키가 필요합니다. 새 노드도 인증서를 발급하는 주체가 되어야 하니까요. kubeadm 은 이 파일을 scp 로 나르게 하지 않고 kubeadm init phase upload-certs --upload-certs암호화해 클러스터 안의 Secret 으로 올리고, 64자리 16진수 certificate-key 를 그 암호문의 열쇠로 줍니다.

그리고 이 Secret 은 2시간 뒤 자동 삭제됩니다. 클러스터의 루트 신뢰인 CA 개인키가 암호화된 상태로나마 클러스터 안에 존재하는 시간을 최소화하려는 설계입니다. "민감한 자료의 노출 창(window)을 줄인다"는 원칙이 제품 기능으로 구현된 좋은 예입니다.

또 하나. 그 홈랩에서는 컨트롤 플레인을 2 대로 늘리지 않고 곧바로 3 대까지 갔습니다. 이유가 명확합니다 — etcd 쿼럼은 과반이고 멤버 2 의 과반은 2 이므로, 2 멤버는 1 멤버보다 장애 확률이 오히려 높습니다. 둘 중 아무나 죽으면 쓰기 불능이 되니까요. 가용성 설계에서 "많을수록 낫다"가 틀리는 대표 사례입니다.

다음 실습에서 할 것

다음 실습에서 apiserver 의 위험 플래그 목록을 직접 작성하고, EncryptionConfiguration YAML 을 프로바이더 순서까지 맞춰 쓰고, kubelet 점검 항목을 정리합니다. 그리고 네임스페이스 두 개에 서비스어카운트와 Role 을 걸어 격리가 실제로 작동하는지 kubectl auth can-i 로 증명하고, 익명 사용자가 지금 이 클러스터에서 무엇을 할 수 있는지 관찰해 기록합니다.