[쿠버네티스] 인증/인가 - OIDC 를 이용한 Keycloak 연동

목차

참고


앞서 말했듯이, 쿠버네티스는 사용자 정보를 저장하지 않습니다. 사용자를 관리하고 싶으면, 쿠버네티스에서 제공하는 OIDC 를 통해 별도의 인증 서버와 연계를 하면 사용자를 관리할 수 있습니다.

인증서버로 유명한 Keycloak 을 이용해 사용자를 관리하고 쿠버네티스에서 인증을 진행할 수 있도록 설정을 진행하려고 합니다.

✅ 1. Keycloak Client 추가

쿠버네티스에서 Keycloak 을 이용해 인증을 진행하기 위해서는 Client 생성(등록) 이 필요합니다. Client 단위로 Keycloak 에서는 쿠버네티스 사용자들을 관리하고 권한을 매핑해줄 수 있습니다.

1-1. Client 생성

kubernetes-client 이름으로 새로운 client 를 생성합니다.

Client authentication 을 활성화 해줍니다.

현재는 OIDC 를 이용하기 위한 URL 이 없으므로 빈칸으로 넘어갑니다.

1-2. Role 생성

새로운 Role 을 추가해줍니다.

1-3. User Client Role 생성

1-4. User 추가

쿠버네티스 사용자 인증을 위해 test 로 계정을 하나 생성합니다.

계정을 활성화 하기 위해서는 password 를 세팅해야 합니다.

1-5. Group 생성

1-6. User 에 Group 매핑

✅ 2. Kubernetes API Server에 OIDC 옵션 추가

2-1. kube-apiserver 매니페스트 변경

쿠버네티스에서 요청을 받은 Kube API Server 가 Keycloak 을 통해 인증의 유효성을 확인할 수 있도록 kube-apiserver.yaml 파일에 설정을 추가해줘야 합니다.

kubeadm으로 설치했을 경우 /etc/kubernetes/manifests/kube-apiserver.yaml 에 파일이 존재합니다.

spec:
containers:
- command:
- kube-apiserver
- --oidc-issuer-url=https://<Keycloak-호스트>/auth/realms/<Realm-이름>
- --oidc-client-id=kubernetes
- --oidc-username-claim=preferred_username
- --oidc-username-prefix=-
- --oidc-groups-claim=groups
- --oidc-groups-prefix="kubernetes:"

2-2. 쿠버네티스 접속을 위한 Keycloak 토큰 생성 테스트

위에서 생성한 Keycloak 에서 사용하기 위한 Client 와 User 정보를 이용해 Token 생성 요청이 정상적으로 이뤄지는지 확인합니다.

curl --location 'https://<Keycloak-호스트>/realms/<Realm-이름>/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password' \
-d 'client_id=kubernetes-client' \
-d 'username=test' \
-d 'password=<비밀번호>' \
-d 'scope=openid' \
-d 'client_secret=<secrete 정보>'

정상적인 응답이 왔을 경우 Keycloak 에서는 아래와 같은 토큰 정보를 반환해줍니다.

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzOEdsMWxzdUNLcFQ4R0RUcDVycF8waEsxN200TmJEOHFqcWp6dnFkMTFjIn0.eyJleHAiOjE3NDM4NjE0NDYsImlhdCI6MTc0Mzg2MTE0NiwianRpIjoiNzZkZTYwYWMtZGMxYy00YjM1LWFiODUtOWM0ZDNjMDc3M2VlIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmRkYW56aXRzLmNvbS9yZWFsbXMvZGRhbnppdCIsInN1YiI6IjI1MzJiNjYxLTA5ODUtNDI1YS1iYWU2LTNjMmQ4YmQ0ZGEyMyIsInR5cCI6IkJlYXJlciIsImF6cCI6Imt1YmVybmV0ZXMtY2xpZW50Iiwic2lkIjoiMzc5ZDYzZWMtNzUwOS00NzUyLWI3YTQtOTM1NDE4YTllOWNkIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZXNvdXJjZV9hY2Nlc3MiOnsia3ViZXJuZXRlcy1jbGllbnQiOnsicm9sZXMiOlsiYWRtaW4iXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJncm91cHMiOlsia3ViZXJuZXRlczphZG1pbiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0In0.K8A_nTdNhP7HR4dR_hvuqGaWspnwnsQgJ5VPiC2UceO2l5tPi4Bxmh_TmeuIjcIOlv-d_CjgyZ4_kuKrobMpinReuAuFLD3IMuzqvXUMTyWkBv-tAxfcHqk4jceUSQEANdrywfzA7Xb2h38aSytm9TglZa7_P9mbNT34r3R_qnjwSxlnlPegjFfYvJFP107Tt5w6GIbIf3XhoRI_TYzDBSVTIvL_Y7lkvvoNB0woF0gBX914qW8_koD3NE0eNpXINhAWUB7LUycvBMYE-edgWj8menY3XwHrcNiO0FHCgTJ2nfA0KgJAspiWD725UnCW5v7Y7wD5rijNYZ38LiVkIQ",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0OGE4YzYzMy0zODk3LTQ3ZjMtYTk3Ni05OTdhNmJkYjQ1MzcifQ.eyJleHAiOjE3NDM4NjI5NDYsImlhdCI6MTc0Mzg2MTE0NiwianRpIjoiZmQ1M2UzNjgtMjM4NC00N2FlLTlhNWYtODEyNjIyNzg0ZmE1IiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmRkYW56aXRzLmNvbS9yZWFsbXMvZGRhbnppdCIsImF1ZCI6Imh0dHBzOi8vYXV0aC5kZGFueml0cy5jb20vcmVhbG1zL2RkYW56aXQiLCJzdWIiOiIyNTMyYjY2MS0wOTg1LTQyNWEtYmFlNi0zYzJkOGJkNGRhMjMiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoia3ViZXJuZXRlcy1jbGllbnQiLCJzaWQiOiIzNzlkNjNlYy03NTA5LTQ3NTItYjdhNC05MzU0MThhOWU5Y2QiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGJhc2ljIHJvbGVzIGFjciB3ZWItb3JpZ2lucyBlbWFpbCJ9.jCsS44jWe8xAUV1BNc4bFaamElEtg7pWHuW-m19azInpg-nziZvUrx1RwMYB_sR6pOWRvSOhoIrZXWLJOml7Vg",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzOEdsMWxzdUNLcFQ4R0RUcDVycF8waEsxN200TmJEOHFqcWp6dnFkMTFjIn0.eyJleHAiOjE3NDM4NjE0NDYsImlhdCI6MTc0Mzg2MTE0NiwianRpIjoiNWU0NTk4OTktOTk5NC00N2FlLWE2NjYtOWEwMzNkZmQ1MThkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmRkYW56aXRzLmNvbS9yZWFsbXMvZGRhbnppdCIsImF1ZCI6Imt1YmVybmV0ZXMtY2xpZW50Iiwic3ViIjoiMjUzMmI2NjEtMDk4NS00MjVhLWJhZTYtM2MyZDhiZDRkYTIzIiwidHlwIjoiSUQiLCJhenAiOiJrdWJlcm5ldGVzLWNsaWVudCIsInNpZCI6IjM3OWQ2M2VjLTc1MDktNDc1Mi1iN2E0LTkzNTQxOGE5ZTljZCIsImF0X2hhc2giOiJ2N3VFMzY2RS1OSmpsa21QbFRJX0dnIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZ3JvdXBzIjpbImt1YmVybmV0ZXM6YWRtaW4iXSwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdCJ9.bAUI2zSw8WGfKtJagjMFMMpza2jHHbPwB5dOGlvC0bgqJANjuZTt8VU43yLps_1ksf3EydOjnnKXoQOPSDrJprdEb6AKud-wzs0KjeuwCWrJXnHEp1YxA9fM8-CTwcExoeFKSHQOIBFkoCAY2F8fUdfmdszqMUj13vm_95QHUwhN8jw12zh_KKS_KwEMODs9fLqTxJLgKHSti7uGQi8CI_NBFDcfC-FP1Q44fzJf7m9trD14NW06iu9IGL4d32xwtcjzdh9d4zM4Wc42bYOfA1wI0l5N-k6jmpYevilWvwACwxLmkHnVdjRlHSS6pb3lY8lyvKSwfMdQs_McMQcsIA",
"not-before-policy": 0,
"session_state": "379d63ec-7509-4752-b7a4-935418a9e9cd",
"scope": "openid profile email"
}

위 여러 토큰 중에서 쿠버네티스 API Server 에 요청시에는 id_token 값을 사용합니다. 해당 토큰에 어떤 정보가 있는지 디코딩 해보면 아래와 같은 값들을 갖고 있는 것을 확인할 수 있습니다.

2-3. 권한 부여 - 롤 생성 및 롤 바인딩

쿠버네티스는 인증 후 리소스를 사용하기 위해서는 권한이 필요합니다. 모든 Pod 에 대한 조회 권한을 주기 위해 ClusterRole 을 생성합니다. 만약, 특정 리소스에 대한 권한만을 주고 싶다고 하면 Role 리소스를 생성하면 됩니다.

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-role
rules:
- apiGroups: [""]
resources: ["namespaces", "pods"]
verbs: ["get", "list", "watch"]

권한이 생성 됐으면 해당 권한을 사용하기 위한 롤 바인딩 리소스도 생성합니다. 위에서 ClusterRol 을 생성했으므로 ClusterRoleBinding 리소스를 통해 바인딩을 해야 합니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keycloak-crb-test
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: keycloak-role
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: test

2-4. 토큰을 이용해 쿠버네티스 API Server 요청

토큰 생성이 정상적으로 되는지 확인했고, 이번에는 생성한 토큰을 이용해 쿠버네티스 API Server 에 요청을 보내려고 합니다.

TOKEN=$(curl --location 'https://<Keycloak-호스트>/realms/<Realm-이름>/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password' \
-d 'client_id=kubernetes-client' \
-d 'username=test' \
-d 'password=<비밀번호>' \
-d 'scope=openid' \
-d 'client_secret=<secrete 정보>' | jq -r '.id_token')

# 받은 토큰을 이용해 쿠버네티스 API Server 에 요청이 정상적으로 가는지 확인해 봅니다.
curl https://<쿠버네티스 api server 주소>/api/v1/namespaces/default --header "Authorization: Bearer ${TOKEN}" --insecure

토큰에 문제가 없을 경우 아래와 같은 응답이 오는 것을 확인할 수 있습니다.

{
"kind": "Namespace",
"apiVersion": "v1",
"metadata": {
"name": "default",
"uid": "1c78224d-d264-4c25-873c-4407be8d63e1",
"resourceVersion": "36",
"creationTimestamp": "2025-02-20T16:16:48Z",
"labels": {
"kubernetes.io/metadata.name": "default"
},
"managedFields": [
{
"manager": "kube-apiserver",
"operation": "Update",
"apiVersion": "v1",
"time": "2025-02-20T16:16:48Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:labels": {
".": {},
"f:kubernetes.io/metadata.name": {}
}
}
}
}
]
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}

인증에 성공한 User 를 이용해 kubectl 명령어를 이용하기 위해 kubeconfig 파일에 토큰 정보를 반영해줍니다.

config
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJ ... DQVRFLS0tLS0K
server: <kubernetes api server 주소>
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: test
namespace: default
name: kubernetes
current-context: kubernetes
kind: Config
preferences: {}
users:
- name: test
user:
token: eyJhbGciO ... GfZXJw # 요청을 통해 생성된 토큰을 추가해 줍니다.

2-5. 인증 및 인가 에러

인증이 정상적으로 이뤄지지 않은 경우에는 다음과 같은 401 에러가 발생하게 됩니다.

{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}%

권한이 제대로 안들어가 있을 경우 403 에러와 함께 리소스에 대한 접근을 할 수 없다는 오류 메시지를 확인할 수 있습니다.

{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "namespaces \"default\" is forbidden: User \"test\" cannot get resource \"namespaces\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"name": "default",
"kind": "namespaces"
},
"code": 403
}%

✅ 3. kubelogin - OIDC 를 이용하기 위한 Client 사용

쿠버네티스에서 Keycloak 을 이용해 사용자 인증과정에 대한 설정이 끝났습니다. 하지만, 사용자가 쿠버네티스로의 인증 방식에 대해서는 현재 딱히 정해진게 없습니다.

위 과정에서 쿠버네티스 리소스를 이용하기 전에 토큰을 생성하고, 토큰을 config 파일에 반영하는 과정들이 있습니다.

매번 반복적으로 하기에는 번거로운 이 과정을 단순화 하기 위해 kubelogin 을 사용해 인증을 간편화하려고 합니다.

3-1. kubelogin 설치.

kubelogin 공식문서 에서 운영체제 별로 설치하는 방법이 있습니다. 저는 맥환경에서 이용하므로 brew 를 이용한 방법만 정리했습니다.

brew install kubelogin

3-2. kubelogin 설치.

kubelogin 에서는 8000 번 포트를 이용해 응답을 대기 합니다. 인증에 성공 후 kubelogin 로 응답을 보내주기 위해 Valid redirect URIs 설정을 추가해줍니다.

3-3. kubeconfig 파일에 정보 추가

쿠버네티스에서 kubelogin 을 이용해 사용자 인증이 진행될 수 있도록 config 파일에 명령어와 옵션들을 추가합니다. 해당 정보들을 통해 kubelogin 에서 Keycloak 을 이용해 인증을 진행하고 인증정보들을 관리해줍니다.

users:
- name: keycloak-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://<Keycloak-호스트>/auth/realms/<Realm-이름>
- --oidc-client-id=kubernetes-client
- --oidc-client-secret=<Keycloak Client Secret>

명령어를 이용해서도 config 파일에 설정정보를 추가할 수 있습니다.

kubectl config set-credentials keycloak-user \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://<Keycloak-호스트>/auth/realms/<Realm-이름> \
--exec-arg=--oidc-client-id=kubernetes-client \
--exec-arg=--oidc-client-secret=<Keycloak Client Secret>

아래 처럼 User 정보를 명시적으로 지정해서 명령어를 사용할 수 있습니다.

kubectl --user=keycloak-user get nodes

kubectl 명령어를 사용하게 되면 kubelogin 가 Keycloak 을 통해 사용자 인증을 하도록 화면을 띄어줍니다.

인증이 정상적으로 이뤄지면 아래와 같이 성공했다는 메시지가 뜨고 위에서 실행한 결과가 정상적으로 실행됩니다.

3-4. 전체 kubeconfig 파일

이제 새로운 쿠버네티스 운영자가 왔을 경우 해당 config 파일을 건내주면 Keycloak 을 통해 사용자 인증을 받고 쿠버네티스 리소스에 접근할 수 있습니다.

apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJ ... DQVRFLS0tLS0K
server: <kubernetes api server 주소>
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: keycloak-user
namespace: default
name: kubernetes
current-context: kubernetes
kind: Config
preferences: {}
users:
- name: keycloak-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://<Keycloak-호스트>/auth/realms/<Realm-이름>
- --oidc-client-id=kubernetes-client
- --oidc-client-secret=<Keycloak Client Secret>
Share