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 추적.
생각해 볼 문제
- 401이 났다. 어느 측(인증/인가)인가? 확인 순서를 세라.
- 회사 전 직원의 클러스터 접근을 관리하려 한다. X.509 vs OIDC 중 어느 쪽이, 왜?
- 1.24+ 클러스터에서 SA를 만들었다. Secret이 안 생겼다. 정상인가? 토큰은 어디서?
- bound SA 토큰이 만료됐다. Pod는 어떻게 되나? (kubelet 갱신)
- OIDC claim의 그룹이 RBAC Group에 매핑된다. 퇴사자 처리는 어떻게 자동화되나?
- 왜 사람은 OIDC고 Pod는 SA 토큰인가? 하나로 통일하면 생기는 문제는?
- Pod가 apiserver에 접근할 때 쓰는 자격과 운영자의 kubeconfig 자격이 다른 이유는? (각각 무엇)
참고
- Kubernetes 공식 문서 - Authenticating - 접근 2026-07-13
- Kubernetes 공식 문서 - Service Account Tokens (bound) - 접근 2026-07-13 (1.24+ 변화)
- Kubernetes 공식 문서 - OIDC 연동 - 접근 2026-07-13
- KEP-1205 Bound Service Account Tokens - 접근 2026-07-13
- Kubernetes 공식 문서 - X.509 client certs - 접근 2026-07-13
'Tech Artifacts > K8s Security' 카테고리의 다른 글
| K8s Security - 01. rbac (0) | 2026.07.16 |
|---|