Authentication

개요

쿠버네티스에서의 모든 명령과 수행 과정은 전부 kube-apiserver를 통해서 이뤄진다.
이때 api서버에서 들어온 요청을 수행할 때 몇 가지 과정을 거치는데, 첫번째 과정이 바로 Authentication, 즉 인증이다.

이 문서에서는 해당 과정에 대한 자세한 내용을 담는다.
추가적으로, 인증 하드닝 가이드[1]에 나온 내용까지 한꺼번에 정리했다.

자기 인증 정보 확인하기

문서에서는 이게 마지막에 위치하지만, 간단하게 인증 정보를 확인하고 싶을 때 사용할 수 있는 수단을 먼저 정리하고자 한다.
자신이 클러스터에서 어떤 계정으로서 인증되는지 알고 싶다면, SelfSubjectReview api를 쓰면 된다.

POST /apis/authentication.k8s.io/v1/selfsubjectreviews

이런 식으로 그냥 http 요청을 날려봐도 되고,

{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "SelfSubjectReview"
}

이런 내용을 바디에 넣어서 POST를 날려도 된다.
kubectl에서는 쉽게 kubectl auth whoami로 확인할 수 있다.
이 기능은 복잡한 인증 흐름을 가지고 있는 클러스터에서는 매우 유용할 것이다.

근데 이것도 이걸 보낼 수 있는 권한을 가진 계정만이 가능하다!
APISelfSubjectReview 피처 게이트가 활성화되어 있으면, 인증된 모든 유저가 이 요청을 날릴 수 있다.
이것은 system:basic-user 클러스터롤에 의해 보장된다.

요청 주체

먼저 클러스터에 요청을 수행할 수 있는 주체는 계정(account)인데, 크게 두 가지로 분류된다.
이렇게 분류한다고 해서 api 서버가 인증 단계에서 둘을 따로 인지하고 처리한다는 것은 아니다.
이 두 유형의 계정 모두 요청을 하더라도 일단 익명의 요청으로 간주되고, 인증 단계를 거치게 된다.

유저 계정

우리 같은 일반적인 사람을 이야기한다.
유저는 여러 방법으로 클러스터에 접근할 수 있다.

쿠버네티스는 이런 유저 계정에 대한 정보를 일체 저장하지 않는다.
유저 이름을 API 접근을 검사하는 다른 단계에서 또 활용하기는 하지만, User 오브젝트를 가지고 있지도 않고, 관련 정보를 api 서버에 절대 보관하지 않는다.
이것 때문에 유의할 것이 하나 있다면, 클러스터에 접근할 수 있는 유저 계정을 관리하고 싶은 조직이라면 별도의 방안을 마련해야 한다.
아래 나오겠지만 외부로부터 인증을 수행하는 다양한 방법들이 있으니 이런 것들을 적절하게 써야 할 것이다.

유저란 표현

앞으로 문서에서 유저라는 표현이 자주 등장할 텐데, 그 유저가 "유저 계정"을 의미하는 게 아닐 때가 많다.
아래 [[#인증 전략]]에서 보면 인증에 확인되는 값들 중에 Username이란 게 떡하니 있다..
즉, 보통 인증 단계에서 유저라고 하면 해당 계정의 신원을 나타낼 때 사용하는 값이다.
그래서 앞으로 만약 유저 계정을 의미하고 싶을 때는 명시적으로 "유저 계정"이라고 명시하겠다.

서비스 어카운트

서비스 어카운트는 쿠버네티스 API를 통해 관리되는 계정이다.
특정 네임스페이스에 달라붙으며, kube-apiserver에 의해 만들어진다.
이 계정에 대한 정보는 Secret에 저장되고, 파드에서 이를 마운팅하여 클러스터 내부에서 파드가 해당 계정으로서 API와 통신할 수 있게 된다.

인증 모듈

쿠버네티스에서는 클라이언트 인증서, 베어러 토큰, 인증 프록시 등을 사용해 인증 요청을 처리한다.
다양한 인증 방법을 마련한 만큼, 이것들을 별도의 모듈로 분리해서 관리할 수 있도록 해두었다.
이 덕분에 관리자는 인증 수단을 여러 개 모듈로 적용할 수 있다.
이 인증 모듈들은 api 요청이 들어왔을 때 차례차례 인증 로직을 수행하는데, 이때 어떤 모듈이 먼저 수행될지에 대한 순서는 보장하지 않는다.
여기에서 주의할 것은 인증 모듈 간의 인증 절차는 OR 연산과 비슷하다는 것이다.
여러 인증 모듈이 있을 때, 하나의 인증 모듈에서만 인증됐다고 표시하면 해당 요청은 인증된 것으로 간주되고 통과된다.

HTTP 요청 자체는 api 서버가 받는다고 할 수 있다.
들어온 요청에 대해 인증 모듈은 다음의 정보를 이용해 인증을 처리할 것이다.
인증 모듈에서 인증을 위해 확인하는 값은 다음과 대체로 다음의 값이다.

인증된 모든 계정은 system:authenticated라는 그룹에 속하게 된다.
반면 인증이 되지 않은 경우는 어떤가?
이 케이스는 두 가지 경우로 나눌 수 있다.

X509 클라이언트 인증서

TLS 통신 과정 상에서 mTLS를 통해 상대의 신원을 검증할 수 있다.
api 서버 역시 이 방법으로 인증을 수행하기도 한다.
클라이언트 인증서는 api서버의 --client-ca-file을 통해 활성화된다.
(왜 CA 파일을 넣는 것으로 활성화되는가는 mTLS 방식을 알면 쉽게 이해할 수 있다.)

이 파일은 반드시 api 서버가 검증을 하기 위한 하나 이상의 인증 기관을 담고 있어야 한다.
만약 인증서가 제시되고 검증됐다면, 유저의 이름은 Common Name 부분의 값으로 정해진다.
1.4부터는 클라이언트인증서는 조직 필드를 통해 유저의 소속 그룹도 나타낼 수 있다.
한 유저에 여러 소속 그룹을 포함하고 싶으면 그대로 조직 필드에 넣어주면 된다.

openssl req -new -key jbeda.pem -out jbeda-csr.pem -subj "/CN=jbeda/O=app1/O=app2"

이런 식으로 openssl을 통해 인증 서명 요청을 만들 수 있다.
이러면 clientname이 jbeda, 조직은 app1, app2일 것이다.
만드는 방법은 여기를 참고하자.[2]

보안 고려 사항

이 방식은 kubelet 과 같은 컴포넌트가 kube-apiserver에 접근할 때 사용하는 방식이다.
이걸 유저 계정에도 사용할 수는 있으나, 다음의 사항을 고려해야 한다.

정적 토큰 파일

정적 토큰 파일은 api서버의 --token-auth-file 인자를 통해 활성화된다.
이 토큰은 무기한 지속되며, 현재는 이 파일은 api 서버를 재시작하지 않고 변경할 방법이 없다.
이 파일은 csv 형식으로 최소 3개의 열을 가지고 있다.

token,user,uid,"group1,group2,group3"

이런 식으로 토큰, 유저, UID에 추가적으로 그룹을 넣어주는 식으로 구성된다.

베어러 토큰을 사용하여 인증 받고 싶은 요청은 HTTP 헤더에 Authorization: Bearer <token>과 같은 식으로 넣어주면 된다.
이것 HTTP 차원에서 일어나는 암호화, 인코딩을 제외해서는 조금의 조작도 이뤄져서는 안 된다.
만약 베어러 토큰이 31ada4fd-adec-460c-809a-9e56ceb75269면 그대로 들어가야 한다는 뜻이다.

보안 고려 사항

위의 예시만 봐도 알겠지만, 사실 이건 진짜 위험한 방식이다.

왜 굳이? 이걸?

솔직히 암만 생각해도 이걸 활용할 이유가 전혀 없는 것 같다.
운영 간 실수로 관리자 계정을 잃어버려서 별도의 대체 계정이 필요하다 치자.
근데 그것도 결국 클러스터 초기화 시 어차피 생기는 루트 유저 인증서가 있는데, 그냥 그거 쓰면 되는 거 아니냐?

부트스트랩 토큰

클러스터를 부트스트랩할 때는 부트스트랩 토큰이라는 베어러 토큰의 한 유형을 동적으로 관리한다.
이 토큰은 kube-system 네임스페이스의 시크릿에 동적으로 생성된고 관리된다.
kube-controller-manager에 TokenCleaner 컨트롤러를 포함하고 있어서 이 토큰이 만료된 이후에 알아서 제거해준다.
이 토큰은 [a-z0-9]{6}.[a-z0-9]{16} 모양을 가지고 있다.
REGEX를 익숙하지 않은 사람들을 위해 말하자면, 숫자나 알파벳으로 6글자 이후 ., 그 이후에 16글자가 등장한다는 것이다.
여기에서 첫 요소는 토큰 ID이고, 두번째 요소가 토큰 시크릿이다.

api서버에는  --enable-bootstrap-token-auth가 명시돼야 부트스트랩 토큰 인증기가 동작한다.
그리고 컨트롤러 매니저에는 --controllers=*,tokencleaner 같은 식으로 인자가 명시돼야 한다.
보통은 kubeadm에서 전부 알아서 해준다.

이 인증 모듈은 system:bootstrap:<Token ID>로서 계정을 인증해준다.
이건 system:bootstrappers 그룹에 들어있다.
이렇게 이름이 제한이 딱 걸린 이유는 유저가 부트스트랩 이후 이 토큰을 사용하는 것을 막기 위함이다.
이 유저 이름과 그룹은 클러스터를 부트스트랩 시키기 위한 적절한 인가 정책을 만들기 위해 쓰인다.
관련한 내용은 여기에 조금 더 담겨있다.[3]

보안 고려 사항

이건 유저 계정에 쓰라고 있는 게 애초에 아니다.
이 토큰은 그룹이나 관련 값들이 하드코딩 된 토큰이라, 애초에 인증 목적으로 사용하기에 적절하지 않다.
초반에 클러스터 초기화될 때만 사용되고, 이후에는 사용되지 않는 게 좋다.

서비스 어카운트 토큰

서비스 어카운트 인증 모듈은 관리자가 설정하지 않아도 자동으로 활성화돼있다.
다음의 인자가 api 서버에 들어가 있을 것이다.

참고로 서비스 어카운트는 관리자가 직접 만들지 않더라도 ServiceAccount Admission Control를 통해 자동적으로 만들어진다.
그리고 그 서비스 어카운트 토큰은 파드가 실행될 때 지정된 위치에 알아서 마운팅되며, 이를 통해 api 서버로의 클러스터 내부 통신이 가능하게 된다.
물론 관리자가 직접 SA를 만들고 파드에 .spec.serviceAccountName으로 원하는 SA를 명시해도된다.

이 토큰은 클러스터 외부에서도 사용할 수 있다.
그리고 쿠버 API와 오래 통신하고 싶을 때 사용하기 적합하다.
kubectl create token <서비스-어카운트>를 통해 서명된 JWT 토큰을 만들 수 있다.
이게 아예 파일로 파드에 마운팅되고, 이를 통해 클러스터 api 에 접근할 수 있게 되는 것이다.
이걸로 그대로 클러스터 외부에서도 쓸 수도 있다는 것.

이렇게 인증되는 유저 이름은 system:serviceaccount:(네임스페이스):(서비스-어카운트-이름)이 된다.
그룹으로서는 system:serviceaccounts, system:serviceaccounts:(네임스페이스)가 된다.

보안 고려 사항

서비스 어카운트 토큰은 시크릿에 저장되므로 이를 탈취하면 쉽게 api 접근이 가능해진다.
그러므로 시크릿에 아무나 권한을 가지는 것에 주의해야 한다.

기본적으로는 이 토큰은 영구 토큰이다.
현재는 TokenRequest API가 있어 이것으로 짧게 토큰을 관리하는 게 가능하니 가능한 이 방식을 사용하자.

OIDC 토큰

OAuth를 지원하는 제공자를 이용해 OIDC를 쓸 수 있다.
이때 ID 토큰을 활용하게 된다.
당연하지만 이 토큰은 JWT 형식으로 돼있다.

로직을 한번 보자.
먼저 유저가 제공자에게 access_token, id_token, refresh_token을 받고, 이 중 id_token을 HTTP 헤더에 Authorization: Bearer <token>과 같은 식으로 요청을 날린다.
그리고 api 서버는 이를 검증한다.
모든 정보는 id_token에 담겨져 있으므로 쿠버네티스에서 인증에 대해 따로 제공자에게 통신을 취할 필요가 없다.
즉, api 서버는 id_token이 들어왔을 때 해당 유저 계정이 이미 인증 제공자로부터 신원이 검증된 상태라고 감안한다는 것이다.
api 서버 입장에서 남은 일은 그렇게 들어온 유저, 그룹 값이 허용된 값인지만 검사하면 된다.

조금 더 예를 들어보겠다.
인증 제공자 쪽에 설정해서 a,b 유저를 만들었다.
이 두 유저는 자신의 ID 토큰을 발급받을 수 있을 것이다.
근데 api 서버에서는 a 유저에 대해서만 인증을 통과시키고 싶다.
그럼 api 서버에서는 a유저가 맞는지만 ID 토큰에서 검사한다는 것이다.

인증 모듈은 ID 토큰을 파싱하고, 일단 설정된 발급자에 의해 서명된 것인지만 검증한다.
서명을 검증하기 위한 공개키는 제공자의 URL 경로로 탐색하여 진행될 것이다.
(이 과정에서는 제공자에 대한 통신이 이뤄지긴 하나, 이것도 그냥 캐시해두는 방법이 있다.)
이후에는 토큰의 claim 필드를 이용해 유저를 인증한다.
최소한의 유효한 JWT 페이로드는 다음과 같은 모양을 띄어야 한다.

{
  "iss": "https://example.com",   // 발급자의 url과 일치해야 한다.
  "aud": ["my-app"],              // 최소한 하나의 원소가 발급자의 청중과 일치해야 한다.
  "exp": 1234567890,              // 유닉스 시간 기준으로 토큰의 만료일시
  "<username-claim>": "user"      // claimMappings.username.claim이나, claimMappings.username.expression에 설정된 유저이름 요청
}

모든 요청은 stateless하기에 인증에 있어서 매우 유연한 솔루션이 된다.
몇 가지 도전과제가 있다.

관련한 api 서버 인자는 다음과 같다.

딱봐도 설정할 인자가 너무 많다...

제한 사항

유의할 점은 쿠버네티스는 자체적으로 OIDC 제공자를 제공하지 않는다.
그래서 구글 같은 외부 OIDC 제공자를 이용해야 한다.
혹은 Keycloak 같은 자체 식별 제공자를 클러스터에 설치해서 이용하는 것도 방법이다.

분산화된 클레임이 있다면 CEL이 잘 동작하지 않는다.
이그레스 셀렉터 설정은 issuer.url, issuer.discoveryURL에 지원되지 않는다.

쿠버네티스에서 OIDC 인증을 위해서 반드시 필요한 사항은 다음과 같다.

kubectl 이용하기

OIDC로 인증받고자 하는 유저가 kubectl을 이용하고 싶다면 아래처럼 해주면 된다.
kubectl에 id_token을 베어러 토큰으로 쓸 수 있도록 설정할 수 있다.
이건 심지어 토큰이 만료되면 재발급도 해준다.

kubectl config set-credentials USER_NAME \
   --auth-provider=oidc \
   --auth-provider-arg=idp-issuer-url=( issuer url ) \
   --auth-provider-arg=client-id=( your client id ) \
   --auth-provider-arg=client-secret=( your client secret ) \
   --auth-provider-arg=refresh-token=( your refresh token ) \
   --auth-provider-arg=idp-certificate-authority=( path to your ca certificate ) \
   --auth-provider-arg=id-token=( your id_token )

이런 식으로 설정해주면 된다.

users:
- name: mmosley
  user:
    auth-provider:
      config:
        client-id: kubernetes
        client-secret: 1db158f6-177d-4d9c-8a8b-d36869918ec5
        id-token: eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw
        idp-certificate-authority: /root/ca.pem
        idp-issuer-url: https://oidcidp.tremolo.lan:8443/auth/idp/OidcIdP
        refresh-token: q1bKLFOyUiosTfawzA93TzZIDzH2TNa2SMm0zEiPKTUwME6BkEo6Sql5yUWVBSWpKUGphaWpxSVAfekBOZbBhaEW+VlFUeVRGcluyVF5JT4+haZmPsluFoFu5XkpXk5BXq
      name: oidc

그럼 kubeconfig 파일이 이런 식으로 설정될 것인데, 달리 말하자면 처음부터 파일을 이렇게 수정해도 된다는 것.
아니면, 처음부터 토큰을 받아와서 --token을 명시해도 된다.

보안 고려 사항

OIDC 인증자를 클러스터 내부에 구축하는 경우, 이들은 대체로 강한 권한을 가져야 하니 워크로드와 명확한 분리가 이뤄질 필요가 있다.
클라우드 벤더마다 OIDC 방식을 제한적으로 사용하게 하는 경우가 많으니 이런 요소를 잘 파악하고 있어야 한다.

웹훅 토큰 인증

웹훅 모듈도 있다!
이 경우, 어떤 요청이 들어오면 지정된 형식으로 웹훅 url에 POST 요청을 때리고, 웹훅을 받은 서버가 인증을 해주길 기대한다.
이것도 헤더의 베어러 토큰을 부분을 날려서 인증해달라 찡찡댈 것이다.

설정 파일은 그냥 kubeconfig 파일 양식으로 작성하면 된다.

apiVersion: v1
kind: Config
# 원격 서비스의 정보를 넣는다.
clusters:
  - name: name-of-remote-authn-service
    cluster:
      certificate-authority: /path/to/ca.pem         # 원격 서비스를 검증하는 CA
      server: https://authn.example.com/authenticate # 쿼리를 보낼 URL

# api 서버의 웹훅 설정을 넣는 필드이다.
users:
  - name: name-of-api-server
    user:
      client-certificate: /path/to/cert.pem # 웹훅 플러그인이 사용할 인증서
      client-key: /path/to/key.pem          # 인증서와 매칭되는 키

# kubeconfig 파일은 컨텍스트가 필요하기에, api 서버에서 하나 제공해야 한다.
current-context: webhook
contexts:
- context:
    cluster: name-of-remote-authn-service
    user: name-of-api-server
  name: webhook

이렇게 설정하면, 클라이언트에서 베어러 토큰을 넣어서 요청을 보내면 인증 웹훅이 json으로 직렬화된 TokenReview 객체에 토큰을 담아 원격 서비스에 POST 요청을 날린다.
웹훅 api는 버전 호환성에 대해 엄격하다.
그래서 요청이 돌아와도 토큰 리뷰 객체의 apiversion이 일치해야 한다.

{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "TokenReview",
  "spec": {
	# 요청의 헤더에 들어있던 토큰
    "token": "014fbff9a07c...", 

    # 청중 식별자에 대한 추가 리스트이다.
    # 청중을 인식하는 토큰 인증자는 청중이 이 리스트 내에 있는지 검증해야 한다.
    # 청중 필드가 없다면, 토큰의 검증은 api 서버에서 이뤄져야 한다.
    "audiences": ["https://myserver.example.com", "https://myserver.internal.example.com"]
  }
}

이게 원격서버로 날아가는 토큰 리뷰 객체 예시이다.
원격 서비스는 여기에 대해 spec 부분은 떼어내고 status 필드를 넣어서 반환한다.

{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "TokenReview",
  "status": {
    "authenticated": true,
    "user": {
      # Required
      "username": "janedoe@example.com",
      # Optional
      "uid": "42",
      # Optional group memberships
      "groups": ["developers", "qa"],
      # 인증자로가 제공하는 추가 정보로, 중요한 데이터를 넣어서는 안 된다.
      # api 로그가 남기 때문이다.
      # 그런 게 필요하다면 admission webhook을 쓰자.
      "extra": {
        "extrafield1": [
          "extravalue1",
          "extravalue2"
        ]
      }
    },
    # 어떤 청중과 일치했는지 반환한다.
    "audiences": ["https://myserver.example.com"]
  }
}

이건 성공 예시이다.
실패하면 authenticated가 false로 온다.

보안 고려 사항

이 방식은 api 서버의 인자를 필수적으로 수정해야 하니 클라우드의 관리형 쿠버네티스에서는 활용할 수 없다.

인증 프록시

마지막으로, api 서버 앞단에 프록시 서버를 배치하는 방법이 있다.
그럼 이 앞단의 프록시가 인증을 수행해도 되는 것 아니겠냐!
그래서 프록시는 허용된 유저를 api 서버로 전달할 때 유저를 X-Remote-User와 같은 헤더에 넣어서 보내주면 된다.
이 헤더를 어떤 값으로 할지에 대한 설정을 할 수 있다.

--requestheader-username-headers=X-Remote-User
--requestheader-group-headers=X-Remote-Group
--requestheader-extra-headers-prefix=X-Remote-Extra-

이리 처음에 설정을 했다면,

GET / HTTP/1.1
X-Remote-User: fido
X-Remote-Group: dogs
X-Remote-Group: dachshunds
X-Remote-Extra-Acme.com%2Fproject: some-project
X-Remote-Extra-Scopes: openid
X-Remote-Extra-Scopes: profile

이런 요청이 들어왔을 때

name: fido
groups:
- dogs
- dachshunds
extra:
  acme.com/project:
  - some-project
  scopes:
  - openid
  - profile

이렇게 받아들인다.

보안 고려사항

주의할 점은 이렇게 들어온 계정은 인증 단계가 그냥 허용된다.
즉, 해커가 헤더에 이렇게 정보를 넣으면 된다~를 알아내서 같은 방식으로 요청을 날리면 고냥 뚫려버린다는것.
그래서 api 서버에서는 프록시 서버의 인증서가 유효한지 명확하게 검증해야 한다.

다른 곳에서 사용된 CA를 함부로 사용하지 말자.

인증 파일 구성

위에 인증 모듈들을 보면, 죄다 api 서버에 인자로 넣어주는 것을 볼 수 있다.
이거 일일히 이렇게 하면 귀찮아서 어떻게 관리하냐, 싶은 당신을 위해! 따로 파일로 인증 모듈을 관리하는 방법이 있다!
--authentication-config 인자를 넣어주면 된다.

참고로 Kubernetes v1.32 - Penelope에서도 아직 인증 설정 파일의 구조 형식이 베타 레벨이다.
이 파일은 아직 모든 방식의 인증 모듈을 지원하지는 않는 것으로 알고 있다.
그래서 StructuredAuthenticationconfiguration 피처 게이트가 비활성화돼있으면 안 된다.
그리고 이를 위해 인자를 넣었다면, 절대 OIDC 관련 인자가 추가적으로 들어가있으면 안 된다.
이 경우 api서버는 바로 에러를 내뿜을 것이다.

이 방식의 장점은 여러 가지가 있다.
일단 CEL 표현을 이용해 유저 속성과 매핑할 수 있게 하여, 쉽게 인증 로직을 추가할 수 있다.
그리고 api 서버는 이 파일이 수정되었을 때 동적으로 인증 모듈을 리로드한다!(이게 진짜 꿀임)
apiserver_authentication_config_controller_automatic_reload_last_timestamp_seconds 메트릭을 통해 마지막으로 리로딩된 시점을 알 수 있다.

다음이 설정 파일의 예시이다.

OIDC 설정 파일 양식

apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
# JWT 호환 토큰을 사용하는 인증기를 리스트로 최대 64개를 넣을 수 있다.
jwt:
  # 발행자가 누군지를 명시하고, 발행자가 발행할 때 토큰에 있어야 하는 값들을 설정하는 부분이다.
- issuer:
    # url은 인증기마다 고유해야 한하며, `--service-account-issuer`의 발급자와 충돌하면 안된다.
    url: https://example.com # --oidc-issuer-url과 같다.
    # 원래 제공자 탐색 요청은 "{url}/.well-known/openid-configuration" 경로로 간다.
    # 그러나 discoveryURL이 명시되면, 이를 덮어쓴다.
    # 그래서 쓸 거면 아주 정확하게 url 경로를 명시해야 한다.
    # 탐색을 통해 가져와진 issuer 필드는  여기 적힌 url 필드와 반드시 일치해야 하며, JWT의 iss 클레임을 검증하는데 사용된다.
    # 이건 잘 알려진 JWT 엔드포인트가 다른 발급자와 다른 위치에서 호스팅되는 시나리오를 위한 것이다.
    # 이 값도 고유해야 한다.
    discoveryURL: https://discovery.example.com/.well-known/openid-configuration
    # --oidc-ca-file와 같은 의미의 필드이다.
    # PEM으로 인코딩된 CA 인증서는 탐색을 하는데 사용된다.
    # 이 값이 없다면 그냥 시스템의 파일을 사용할 것이다.
    certificateAuthority: <PEM encoded CA certificates>   
    # 청중은 JWT가 반드시 발급 받아야 하는 값이다.
    # JWT 클레임의 aud가 반드시 이 값에 있어야 해당 토큰이 유효하다고 판단될 것이다.
    audiences:
    - my-app # --oidc-client-id와 같다.
    - my-other-app
    # 여러 청중이 명시되었을 때는 반드시 MatchAny를 써야 한다.
    audienceMatchPolicy: MatchAny
  # 유저를 인증하기 위해 토큰 클레임을 검증할 때 적용되는 정책
  claimValidationRules:
    # --oidc-required-claim key=value와 같으며, 이러면 hd: example.com 일 때 유효할 것이다.
  - claim: hd
    requiredValue: example.com
    # 이 방식 대신 진위를 판별하는 CEL 표현식으로 사용해도 된다.
  - expression: 'claims.hd == "example.com"'
    # 검증이 실패했을 때 api서버 로그에 나올 에러 메시지 커스텀
    message: the hd claim must be set to example.com
  - expression: 'claims.exp - claims.nbf <= 86400'
    message: total token lifetime must not exceed 24 hours
  # 클레임 매핑
  claimMappings:
    # username은 유저이름 속성을 대표하는 옵션으로, 이 필드는 필수 요구된다.
    username:
      # --oidc-username-claim와 같으며, username.expression과 양립할 수 없다.
      claim: "sub"
      # --oidc-username-prefix와 같으며 username.expression과 양립할 수 없다.
      # 위의 클레임이 설정됐을 때는 이것도 같이 있어야 하며, 아무것도 안 넣고 싶으면 ""를 넣어라.
      prefix: ""
      # CEL  표현식으로 매칭할 수 있다.
      # 1.  'claims.email'을 쓸 거라면 'claims.email_verified'가 유저네임 표현식에 들어가야한다.
      #     아니면 extra[*].valueExpression이나 claimValidationRules[*].expression이 들어가야 한다.
	  #     이건 유저네임 클레임 검증이 email로 됐을 때 자동 적용되는 CEL 표현식 예시이다.'claims.?email_verified.orValue(true)'
      # 2.  username.expression이 빈 문자열이면, 인증은 실패한다.
      expression: 'claims.username + ":external-user"'
    # 그룹 속성에 대한 옵션으로, 여기서부터는 선택
    groups:
      # --oidc-groups-claim와 같다. 위와 같이 expression과 같이 있을 수 없다.
      claim: "sub"
      prefix: ""
      # 문자열 리스트를 평가하는 표현식이어야 한다.
      expression: 'claims.roles.split(",")'
    # uid 속성에 대한 옵션
    uid:
      claim: 'sub'
      # 이것도 둘이 같이 못있는다.
      expression: 'claims.sub'
    # UserInfo 객체에 추가되는 추가 속성으로, 키는 반드시 고유한 도메인 형식이어야 한다.
    extra:
      # / 전까지는 도메인이고, 이후에는 전부 경로로 간주된다.
      # k8s.io, kubernetes.io 는 쿠버네티스에 예약되었으므로 사용할 수 없다.
      # 전부 소문자로, extra 사이에서 고유해야 한다.
    - key: 'example.com/tenant'
      # 리스트를 검증하는 표현식이면 된다.
      valueExpression: 'claims.tenant'
  # 최종 유저 객체에 적용되는 검증 규칙.
  userValidationRules:
    # 진위를 판별하는 CEL 표현이면 된다.
    # 여기에 해당하는 모든 표현식을 통과해야 유효하다고 판정된다..
  - expression: "!user.username.startsWith('system:')"
    message: 'username cannot used reserved system: prefix'
  - expression: "user.groups.all(group, !group.startsWith('system:'))"
    message: 'groups cannot used reserved system: prefix'

여기에서 조금 정리를 하자.
각각 issuer.url, issuer.discoveryURL을 다르게 설정할 수 있을 것이다.

일단 토큰이 오면, 이것을 뜯어볼 수 있다.
이때 claims에 있는 부분을 이용해서 UserInfo라는 객체를 만들고, 이것을 이용해 최종 검증을 실시한다.
근데 일단 claim에 원하는 내용이 있긴 한지 체크할 때 jwt[*].claimValidationRules를 적용한다.
그리고 UserInfo 객체를 만들기 위해 claims의 내용을 따서 객체의 값으로 매핑하는 과정이 필요하다.
이것이 바로 jwt[*].claimMappings이다.
이후에 만들어진 Userinfo 객체를 검증하는 과정이 jwt[*].userValidationsRules이다.

익명 요청 설정 파일 양식

만약 파일로 인증 설정을 하게 된다면, --anonymous-auth 인자를 설정할 수 없다.
파일로 관리할 때의 장점은 사용할 엔드포인트 지정을 할 수 있다는 것이다.

apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
anonymous:
  enabled: true
  conditions:
  - path: /livez
  - path: /readyz
  - path: /healthz

이런 식으로 말이다.
이렇게 경로를 명시하면, RBAC와 상관없이 다른 경로는 전부 접근이 불가능해진다.

유저 흉내(impersonation)

인증된 유저는 적절한 권한을 가진 경우에, 다른 유저처럼 행동해보는 게 가능하다.
대체로 이 권한은 관리자만 가질 수 있는데, 쿠버네티스 인가 단계에서 impersonate라는 동사에 대한 권한이 있어야 한다.
관리자가 이 기능을 써서 설정된 다른 유저가 적합한 권한을 가지고 있는지 디버깅하는데 활용할 때 유용할 것이다.

흉내 요청은 먼저 요청하는 유저를 검증하고, 이후에 흉내낼 유저 정보로 탈바꿈되는데, 구체적으론 다음의 로직을 따른다.

과정을 보면, 일단 인가 단계로 요청이 넘어가긴 한다는 것을 알 수 있다.
그 다음 흉내 권한이 있는 경우 다시 인증 단계로 돌아와서 각종 정보가 탈바꿈된채 다시 인증 수행 절차에 들어가는 것이다.

다음의 HTTP 헤더가 사용된다.

Impersonate-User: jane.doe@example.com
Impersonate-Extra-dn: cn=jane,ou=engineers,dc=example,dc=com
Impersonate-Extra-acme.com%2Fproject: some-project
Impersonate-Extra-scopes: view
Impersonate-Extra-scopes: development
Impersonate-Uid: 06f6ce97-e2c5-4ab8-7ba5-7654dd08d52b

가령 이런 식으로 작성하면 된다.
kubectl에서는 --as, --as-group을 이용해서 같은 동작을 수행할 수 있다.
다만 추가 헤더를 전달할 수는 없다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: impersonator
rules:
# 흉내를 위한 롤
- apiGroups: [""]
  resources: ["users", "groups", "serviceaccounts"]
  verbs: ["impersonate"]
# 추가 헤더와 uid를 넣기 위한 롤
- apiGroups: ["authentication.k8s.io"]
  resources: ["userextras/scopes", "uids"]
  verbs: ["impersonate"]

이러한 클러스터롤이 있어야만 흉내낼 수 있다.
어떤 유저로 흉내를 내야 하기 때문에, 네임스페이스에 종속되지 않는다.
그래서 클러스터롤로 만들어야 하는 것이다.
각 롤에 아예 resourceNames를 명시해서 어떤 걸로 흉내낼 수 있는지도 커스텀할 수 있다.

client-go 신원 플러그인

이건 아직 잘 모르겠다

경고!!

미완성된 글입니다!!
추가 작성해야 하는 글입니다!!!

관련 문서

이름 noteType created
Authentication knowledge 2025-01-13
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 published 2025-03-15
6W - api 구조와 보안 1 - 인증 published 2025-03-15
6W - EKS api 서버 접근 보안 published 2025-03-16
E-쿠버네티스 인증 실습 topic/explain 2025-01-21
T-서비스 어카운트 토큰은 어떻게 인증되는가 topic/temp 2025-03-16

참고


  1. https://kubernetes.io/docs/concepts/security/hardening-guide/authentication-mechanisms/ ↩︎

  2. https://kubernetes.io/docs/tasks/administer-cluster/certificates/ ↩︎

  3. https://kubernetes.io/docs/reference/access-authn-authz/bootstrap-tokens/ ↩︎

  4. https://github.com/int128/kubelogin ↩︎