Tech Artifacts/K8s Security

K8s Security - 02. authentication

"누구인가"를 증명하는 세 가지 방법 — Kubernetes 인증의 지형

한 클러스터에 세 종류의 접속자가 있었다: 운영자(사람), CI/CD 파이프라인(자동화), 그리고 Pod 안의 앱. 셋 다 apiserver에 접근하지만 증명 방식이 달랐다 — 운영자는 회사 SSO(OIDC), CI는 서비스 계정 토큰, 앱은 마운트된 토큰. Kubernetes는 이 셋을 다른 방식으로 식별한다. "인증"이 하나가 아니라는 것을 모르면, 접속이 안 되는 이유를 엉뚱한 곳에서 찾는다.

이 글이 푸는 것은: Kubernetes가 "누구인가"를 증명받는 여러 방식(X.509, OIDC, Service Account token)과 그 차이다. 특히 왜 세 방식이 다른가 — 접속자의 성격(사람/자동화/시스템)이 방식을 결정한다는 인과를 푼다. 인증(authn)은 "신원", RBAC(01장)은 "권한" — 이 둘이 분리된다는 것이 핵심이다.

인증(authn)과 인가(authz)는 다르다

flowchart LR
    REQ["요청<br/>(토큰/인증서)"] --> AUTHN["1. 인증 (Authentication)<br/>이건 누구인가?"]
    AUTHN --> AUTHZ["2. 인가 (Authorization/RBAC)<br/>이 사람이 이걸 해도 되나?"]
    AUTHZ --> ADM["3. 어드미션 컨트롤<br/>(04번)"]
    ADM --> API["apiserver 처리"]
  • 인증(who): 요청자의 신원 확인. 이 글.
  • 인가(can): 그 신원이 이 동작을 할 권한이 있는가. 01장 RBAC.

이 둘이 분리돼 있어, 인증이 실패하면(잘못된 토큰) "권한 없음"이 아니라 "인증 실패"가 난다. 반대로 인증은 됐는데 권한이 없으면 403. 이 구분이 디버깅의 첫 분기다. (Kubernetes docs - Authenticating)

왜 인증 방식이 하나가 아닌가 — 접속자의 성격이 다르다

도입의 세 접속자로 돌아가자. 왜 운영자는 OIDC고, CI는 SA 토큰이고, 시스템 구성요소는 X.509인가? 하나의 방식으로 통일하면 안 되나?

안 된다. 각 접속자의 수명관리 주체가 다르기 때문이다:

  • 사람(운영자): 입사/퇴사가 있고, 권한이 자주 바뀐다. 퇴사자의 접근을 즉시 차단해야 한다. 그래서 외부 IdP(회사 SSO)가 사용자를 관리하고, Kubernetes는 그것을 신뢰만 한다.
  • 자동화(Pod/CI): 사람처럼 퇴사하지 않지만, 토큰이 탈취되면 큰일이다. 그래서 짧은 수명의 토큰을 쓰고, Pod와 묶어 자동 갱신한다.
  • 시스템 구성요소(kubelet 등): 클러스터 안의 강한 신뢰 관계. 인증서 기반으로, 클러스터 내부에서만 유효.

이 세 성격이 다르므로, 증명 방식도 세 가지로 나뉜다. 각각 어떤 문제를 푸는지 보자.

사람은 외부 IdP에 맡긴다 — OIDC

사용자 인증을 Kubernetes가 직접 하면: 사용자 추가/제거/비밀번호 변경을 클러스터에서 해야 한다. 퇴사자가 생기면 클러스터에서 수동으로 차단. 여러 클러스터가 있으면 각각에서. 이건 비효율적이다.

그래서 Kubernetes는 사용자 인증을 외부에 맡긴다. OIDC(OpenID Connect)로 외부 IdP(Identity Provider — Google, Keycloak, Okta 등)가 토큰(JWT)을 발급하면, apiserver가 그것을 검증해 신원을 식별한다. (Kubernetes docs - OIDC)

# apiserver에 OIDC 설정 (클러스터 구성 시)
--oidc-issuer-url=https://idp.example.com \
--oidc-client-id=kubernetes \
--oidc-username-claim=email \
--oidc-groups-claim=groups

운영자는 회사 SSO로 로그인해 토큰을 받고, 그 토큰으로 kubectl을 쓴다. 신원/그룹 정보가 JWT claim에서 온다.

OIDC claim이 RBAC로 매핑되는 과정

OIDC가 "회사 SSO로"라는 건 알겠다. 그런데 OIDC 토큰의 정보(claim)가 어떻게 RBAC 권한이 되나? 구체적 흐름:

flowchart LR
    USER["사용자<br/>(브라우저)"] --> IDP["IdP 로그인<br/>(Google/Keycloak)"]
    IDP --> TOK["OIDC 토큰(JWT)<br/>claim: email, groups"]
    TOK --> KUBECTL["kubectl<br/>(토큰 제시)"]
    KUBECTL --> APIS["apiserver<br/>(--oidc-username-claim=email<br/>--oidc-groups-claim=groups)"]
    APIS --> IDENTITY["신원: alice@corp / groups:[dev-team]"]
    IDENTITY --> RBAC["RBAC: dev-team 그룹에<br/>edit 권한이 있는가?"]

apiserver 설정(--oidc-username-claim, --oidc-groups-claim)이 "토큰의 어느 claim을 사용자 이름/그룹으로 쓸까"를 정의한다. 이 매핑이 OIDC 신원 → Kubernetes Subject → RBAC 권한의 다리다.

실무의 힘: IdP에서 사용자를 dev-team 그룹에 넣으면 — Kubernetes RBAC에 dev-team Group에 edit 권한을 준 것과 매칭된다. 퇴사 시 IdP에서 사용자 비활성화하면 즉시 Kubernetes 접근 차단 — Kubernetes에서 User 객체를 따로 관리 안 해도. 이것이 OIDC가 "사람 사용자 관리의 베스트 프랙티스"인 이유.

01장에서 "User 객체가 Kubernetes에 없다"고 했다. 이제 이유가 보인다 — 사용자는 외부 IdP가 관리하므로, Kubernetes는 이름만 식별하면 된다. User를 kubectl create user로 만들 수 없는 것도 이 때문.

자동화는 짧은 수명 토큰으로 — Service Account

Pod와 CI/CD 파이프라인은 사람이 아니다. 퇴사하지 않지만, 토큰이 탈취되면 자동화 권한으로 클러스터를 건드릴 수 있다. 이 접속자의 신원이 ServiceAccount(SA)다. User와 달리 SA는 객체로 존재 — namespace 안에 SA 객체가 있고, kubectl create serviceaccount로 만든다. (Kubernetes docs - Service Account Tokens)

과거: 영구 토큰의 문제 (1.23 이전)

과거엔 SA를 만들면 자동으로 Secret에 영구 토큰이 들어갔다. Pod는 그 Secret을 마운트해 apiserver에 접속했다. 문제: 영구 토큰은 탈취되면 취소하기 전까지 계속 유효했다. 만료도 없고, Pod가 죽어도 토큰은 살아있었다. (KEP-1205 Bound Service Account Tokens)

현재: Bound Service Account Token (1.24+)

영구 토큰의 문제를 풀기 위해 ProjectedServiceAccountToken이 도입됐다 — Pod에 토큰을 볼륨으로 마운트(자동). 핵심 변화:

  • 만료 있음: 짧은 수명(JWT, 시간 단위 만료). 영구가 아니다.
  • 대상 제한(audience): 특정 대상(apiserver)용으로만 유효.
  • Pod와 묶임: Pod가 죽으면 토큰도 무효.
  • Secret 없음: 1.24부터 SA를 만들어도 Secret이 자동 생성 안 된다.
# Kubernetes 1.36 — Pod에 자동 마운트된 SA 토큰
kubectl exec <pod> -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -d. -f2 | base64 -d 2>/dev/null

확인할 것: JWT 형식 토큰. exp(만료), aud(대상), pod 이름 등이 claim에 들어있다.

kubelet이 만료 전에 토큰을 자동 갱신해 주므로, Pod는 토큰 만료를 의식하지 않아도 된다. 이것이 "자동화 신원은 짧은 수명 + 자동 갱신" 원칙의 구현이다.

이 변화(1.24+)가 중요한 이유: 과거 자료가 "SA를 만들면 Secret에 토큰이 있다"고 가르치는데, 1.24+에선 그렇지 않다. Pod 마운트 방식으로 읽어야 한다. 구형 자료를 따라하면 "Secret이 안 생겼다"며 혼란.

시스템 구성요소는 인증서로 — X.509

kubelet, controller-manager, scheduler 같은 시스템 구성요소도 apiserver에 접속한다. 이들은 클러스터 내부의 강한 신뢰 관계에 있고, 인증서 기반 인증이 자연스럽다.

kube-apiserver를 켤 때 --client-ca-file로 CA 인증서를 지정하면, 그 CA로 서명된 클라이언트 인증서를 제시한 요청을 인증한다. 인증서의 CN(Common Name)이 사용자 이름, O(Organization)이 그룹이 된다. (Kubernetes docs - X.509 client certs)

# kubeconfig의 인증서가 이 방식 (kubeadm이 만든 admin.conf 등)
kubectl config view --minify --raw | grep client-certificate

X.509는 kube-system 구성요소가 apiserver에 접속할 때 흔히 쓰인다. 단점: 인증서 갱신/취소가 번거롭다(재발급 필요). 사람 사용자용으론 잘 안 쓴다 — 갱신 부담 때문에 OIDC가 선호된다.

인증서 갱신과 kubeconfig — 운영자의 정기 과제

운영자의 kubeconfig(admin.conf 등)도 X.509 인증서 기반이면 만료가 있다. kubeadm 클러스터의 인증서는 기본 1년. 만료 전 kubeadm certs renew로 갱신해야 한다(14장). 이것을 잊으면 — 어느 날 kubectl이 갑자기 "인증서 만료"로 안 된다.

이것이 07장(cert-manager)이 시스템 인증서 자동화를 다루지만 kubeadm 시스템 인증서는 cert-manager가 직접 관리 안 하는 이유다. 운영자의 kubeconfig/클러스터 인증서 만료 알림이 필수적이다.

새 노드의 일회용 신원 — bootstrap token

kubeadm(14장)에서 kubeadm join이 토큰을 쓴다. 이 bootstrap token이 특수하다 — 새 노드가 처음 클러스터에 합류할 때 일회용으로 쓰는 신원이다.

flowchart LR
    NODE["새 노드"] -->|"1. bootstrap token 제시"| APIS["apiserver"]
    APIS -->|"2. 일회용 신원 확인"| CSIGN["인증서 서명"]
    CSIGN --> NODE["3. 노드 전용 인증서 획득"]
    NODE -->|"4. 이후부턴 인증서로"| APIS
  • 만료 짧음(기본 24시간).
  • 합류 후엔 노드가 자기 전용 인증서를 받아, 이후부턴 그 인증서로 인증(토큰은 버림).

왜 영구적이면 안 되나? bootstrap token이 탈취되면 공격자가 언제든 노드를 합류시킬 수 있다. 그래서 짧은 만료 + 일회용로 설계됐다. 이 "bootstrap → 영구 신원" 전환이 노드 등록의 표준이다.

인증 실패의 두 종류 — 401과 403

인증이 안 되는 이유는 다양하지만, 디버깅에 중요한 구분이 있다:

  • 401 Unauthorized: 인증 실패. 누구인지 못 증명(잘못된 토큰/인증서). 원인 명확.
  • 403 Forbidden: 인증은 됐는데 권한 없음(RBAC 거부).

이름이 헷갈린다 — HTTP 401이 "Unauthorized"인데 사실 인증 실패다. 403이 진짜 "권한 없음". 403이면 인증은 통과한 것이므로 RBAC(01장)을 봐야 한다. 이 구분이 첫 디버깅 분기다. 401 → 토큰/인증서/IdP 연동 점검. 403 → RBAC 권한 점검.

익명 요청 (anonymous)

apiserver가 --anonymous-auth=true(기본)면, 인증 정보 없는 요청을 system:anonymous 사용자로 식별한다. RBAC에서 허용하지 않으면 거부된다. 보통은 안전하지만, 인증 없이 접근 가능한 엔드포인트(예: /healthz)에 쓰인다. 과도한 허용은 위험하다.

impersonation — 다른 신원으로 행세 (위험한 능력)

Kubernetes엔 impersonation 기능이 있다 — 한 User가 다른 User의 신원으로 요청. --as 플래그:

kubectl get pods --as=alice --as-group=dev-team

admin이 "alice의 권한으로 저걸 보면 어떻게 되나?" 테스트할 때 쓴다. RBAC 권한 점검의 유용한 도구.

그런데 impersonation 자체가 권한이다 — impersonate verb. 이것을 너무 넓게 주면, 한 사용자가 다른 모든 사용자를 가장해 권한을 우회할 수 있다. 그래서 impersonate 권한은 극히 제한적으로 줘야 하고, audit log(10장)가 impersonation 사용을 추적해야 한다.

token review — 커스텀 인증 통합의 API

Kubernetes 인증을 커스텀으로 확장하려면(자사 SSO 등) TokenReview API를 쓴다. 외부 인증 서버가 토큰을 받아 apiserver의 TokenReview 엔드포인트에 질의 — "이 토큰이 유효한가, 누구인가?" apiserver가 응답한다. 이 webhook 인증으로 Kubernetes 인증을 임의의 시스템과 연동할 수 있다. OIDC가 커버 못 하는 커스텀 인증 흐름에 쓰인다.

인증 지형 정리 — 누가 무엇을 쓰나

접속자 주 인증 방식 왜 그 방식인가 만료/갱신
운영자(사람) OIDC(회사 SSO) 퇴사/권한 변경을 IdP가 중앙 관리 IdP가 관리(퇴사 시 즉시 차단)
CI/CD 파이프라인 SA token(bound, 1.24+) 자동화 신원, 짧은 수명 자동 갱신
Pod 안 앱 SA token(마운트) Pod와 묶임, 자동 갱신 kubelet이 자동 갱신
새 노드 bootstrap token(일회용) 합류 후 폐기, 영구면 위험 24시간 만료, 합류 후 폐기
시스템 구성요소 X.509 인증서 클러스터 내부 강한 신뢰 kubeadm/cert-manager가 갱신

이 표가 "누가 무엇으로 인증하나, 왜"의 전체 지형이다. 각 접속자가 적합한 방식을 쓰고, 만료/갱신 관리가 각기 다르다. 이 매핑을 모르면 "갑자기 접속이 안 된다"(만료)를 진단하지 못한다.

직접 확인하기

# Kubernetes 1.36 — 인증 방식 확인
# 1. kubeconfig의 X.509 (kubeadm/kind 기본)
kubectl config view --minify | grep -E 'client-certificate|token'
# 2. Pod의 SA 토큰 (bound token)
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- \
  ls /var/run/secrets/kubernetes.io/serviceaccount/

확인할 것: Pod에 token/ca/namespace 파일이 자동 마운트됨(1.24+ bound token).

# 인증 실패(401) vs 권한 거부(403) 직접 겪기
kubectl get pods --as=nonexistent-user 2>&1 | head -1   # 권한(403) — 인증은 됨(익명으로)

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"인증과 인가는 같다" 다름. 인증=who, 인가=can(RBAC, 01장)
"SA를 만들면 Secret에 토큰이 생긴다" 1.24+에선 안 생김. Pod 마운트(bound token) 방식
"OIDC는 Kubernetes 자체 기능이다" 외부 IdP와 연동. Kubernetes는 검증만
"User 객체를 만들어야 로그인된다" User 객체 없음. X.509/OIDC가 이름만 식별
"SA 토큰은 영구다" bound token은 만료 있음. kubelet이 자동 갱신
"403이면 인증이 안 된 것이다" 403은 권한 거부. 인증 실패는 401
"모든 접속자를 같은 방식으로 인증해야 한다" 접속자 성격(수명/관리 주체)이 다르므로 방식이 다름

요약 — 이 글의 결론

  • 인증(authn)은 "누구", 인가(authz)는 "can". 둘은 분리. 401=인증 실패, 403=권한 거부로 디버깅 분기.
  • 인증 방식이 세 가지인 이유는 접속자의 성격이 다르기 때문 — 사람(퇴사/권한 변경 → OIDC), 자동화(짧은 수명/자동 갱신 → SA bound token), 시스템(내부 신뢰 → X.509). 하나로 통일할 수 없다.
  • OIDC 연동이 사람 사용자 관리의 베스트 프랙티스 — 회사 SSO로, IdP가 중앙 사용자 저장소 역할. 퇴사 시 즉시 차단. User 객체가 없는 이유.
  • Service Account token은 1.24+에서 변화 — 영구 Secret 토큰 → 만료 있는 bound token(Pod 마운트). 과거 자료와 다름. kubelet이 자동 갱신.
  • X.509는 시스템 구성요소(kubelet 등)의 인증 방식. 갱신 부담이 있어 사람용으론 OIDC가 선호.
  • bootstrap token이 새 노드의 일회용 신원 — 짧은 만료 + 합류 후 폐기. 영구적이면 탈취 위험.
  • impersonation은 권한 점검 도구이지만, 그 자체가 위험한 권한. 제한적 부여 + audit 추적.

생각해 볼 문제

  1. 401이 났다. 어느 측(인증/인가)인가? 확인 순서를 세라.
  2. 회사 전 직원의 클러스터 접근을 관리하려 한다. X.509 vs OIDC 중 어느 쪽이, 왜?
  3. 1.24+ 클러스터에서 SA를 만들었다. Secret이 안 생겼다. 정상인가? 토큰은 어디서?
  4. bound SA 토큰이 만료됐다. Pod는 어떻게 되나? (kubelet 갱신)
  5. OIDC claim의 그룹이 RBAC Group에 매핑된다. 퇴사자 처리는 어떻게 자동화되나?
  6. 왜 사람은 OIDC고 Pod는 SA 토큰인가? 하나로 통일하면 생기는 문제는?
  7. Pod가 apiserver에 접근할 때 쓰는 자격과 운영자의 kubeconfig 자격이 다른 이유는? (각각 무엇)

참고

'Tech Artifacts > K8s Security' 카테고리의 다른 글

K8s Security - 01. rbac  (0) 2026.07.16