23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
Kubernetes를 설치하면, kubeconfig 라는 Kubernetes 접근에 대한 설정이 Home Directory 및 .kube라는 Directory 및에 config란 이름으로 저장되어 있는 것을 발견 할 수 있습니다.
kubeadm 으로 Kubernetes를 설치한 후 아래와 같은 메시지를 확인하실 수 있습니다.
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.confconfig 파일을 보면, 아래와 같은 정보로 구성되어 있습니다.
kubectl 이란 CLI는 Kube API Server에 접속하는 Client 입니다. 통상 K8S API Server는 통상 Private CA를 구성하여 사용함으로, Client에게 Private CA(K8S CA)정보와 Client가 사용할 Client인증서/Key정보를 전달해 주는 것 입니다.
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: ## base64로 코딩된 K8S CA 인증서
server: https://k8s.foo.com:6443 ## kube-api Server의 주소와 포트
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: ## base64로 코딩된 K8S CA가 발행 한 Client 인증서
client-key-data: ## base64로 코딩된 Client 인증에 사용된 Private Key 좀더 자세히 탐색해 보겠습니다. Client 인증서에는 Client가 누구인지를 나타내는 항목이 있습니다. K8S는 인증서의 Subject(인증대상) 중 CN항목을 User, O항목을 Group으로 판단합니다.
config 파일의 client-certificate-data 의 값을 Decoding하면 다음과 같은 Subject를 가지고 있습니다.
O=kubeadm:cluster-admins, CN=kubernetes-admin이름만 봐도, Admin 권한인 것을 유추 할 수 있습니다. 탐색해 보는 김에 K8S의 ClutserRoleBinding과 ClusterRole을 확인합니다. 뒤에 말씀드리겠지만, K8S는 RBAC를 제공합니다. ClusterRole이란 권한이고, ClusterRoleBinding은 권한과 특정 대상과의 Mapping을 정의 한 것 입니다.
먼저 ClusterRoleBinding을 탐색해 봅시다. 아래와 같이 kubectl client가 속한 group인 kubeadm:cluster-admins이 cluster-admin이라는 ClusterRole를 갖습니다.
$ kubectl describe clusterrolebinding kubeadm:cluster-admins
Name: kubeadm:cluster-admins
Labels: <none>
Annotations: <none>
Role:
Kind: ClusterRole
Name: cluster-admin
Subjects:
Kind Name Namespace
---- ---- ---------
Group kubeadm:cluster-admins그럼 cluster-admin 이란 ClusterRole이 어떤 권한을 갖는지 알아봅시다.
모든 자원에 대한 모든 권한을 가진 Role 입니다. 우리가 추측했듯이 Admin(Super User)권한입니다.
$ kubectl describe clusterrole cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
[*] [] [*]우리는 이번 탐험으로 K8S는 X509 인증서 기준 상호인증을 제공하고, User/Group별 RBAC을 제공하는 것을 확인 했습니다. 그렇다면 User/Group은 어떻게 관리해야 할 까요? user별 config 파일은 누가 어떻게 전달해야 할 까요?
조금 찾아 보니 K8S 자체는 User, Group이란 자원을 별도로 관리하지 않습니다. 뭘 어찌하라는 것 일까요?
본 기고를 통해 차근차근 알아보도록 하겠습니다.
Kubernetes 사용자 관리에 대해서 알아 가기전에 Kubernetes의 구조에 대해서 복습해 보겠습니다.
아래는 간단하게 도식화된 Kubernetes의 구조 입니다. Kubernetes는 API Server를 통해 Resource를 변경하는 시스템입니다. 즉 공용 게시판에 할일을 적으면 그림의 스폰츠밥(Client)들이 알아서 할일을 하고, 일을 마무리한 후, 작업명세서(리소스)의 상태를 변화 시키는 것 입니다. 결국은 어떤 Task를 행한다는 건 결국 중앙의 resource의 상태를 변화시키는 것으로 귀결됩니다.
따라서, Kubernetes의 인증/인가/승인은 API의 인증/인가/승인 입니다.
Authentication은 인증입니다. 즉 API의 접근이 허가된 대상을 확인하는 절차 입니다.
Kube API Server는 REST 형태의 API를 제공합니다. 따라서 일반적인 Web의 API 인증과 동일한 기술을 사용합니다.
우리가 우리가 누구인지를 인증하기 위해서는 결국 신분증이 필요합니다. 따라서 인증을 이해하려면 어떤 종류의 신분증을 사용하는지 알아 보면 됩니다.
K8S는 2가지 종류의 신분증을 인증에 사용합니다.
1탄에서 언급된 Client 인증서 기반 인증입니다. Client인증서에는 Subject 항목 중, CN 항목에 User를 O항목에 Groups를 넣어서 자신이 누구인지 주장하며, 공개키 기반 전자서명이 포함되어 있습니다. 앞서 살펴본 Kubeconfig 파일에 들어 있는 인증 방식 입니다. 이 방식은 강한 보안을 제공하나, 미리 K8S CA 인증서를 필요한 Client에게 배포하고 K8S CA가 사용자 별로 Client 인증서를 발급하고 배포해야 합니다.
K8S는 인증서 배포 및 관리 기능을 제공하고 있지 않습니다. 또한 K8S CA와 같은 Private Root CA를 배포하고 안전하게 관리하기는 어렵기 때문에, 일반적인 사용자 인증용으로 쓰기에는 어려움이 있습니다.
HTTP Header에 Token (미리 약속된 증표)를 포함하여, 매 API Call 마다. 포함하여 인증을 하는 방식입니다. K8S는 두가지 종류의 Token을 지원합니다.
HTTP Header에 K8S에 미리 저장된 Token(그냥 미리 저정한 임의의 문자열 이라 생각하면 됩니다.)를 삽입하는 형태 입니다. 미리 정한 Token은 Static token file의 형태로 kube api server의 실행 시, 실행 argument로 입력됩니다.
아래는 token file은 csv 파일 형태로, 각 raw는 아래와 같은 형식을 갖습니다.
token,user,uid,"group1,group2,group3"이 방식의 경우, User / Group의 CURD시 마다 API Server를 재기동해야 합니다. 또한 HA 구조의 Cluster의 경우, kube API Server가 구동되는 Machine에 token 파일을 안전하게 배포/관리하는 기술이 별도로 필요합니다.
JWT 형식으로 Decoding된 Token을 사용합니다. JWT Token은 보다 유연한 Structure를 제공하고, 다양한 전자 서명을 제공합니다.
K8S에서 구동되는 Container에서 K8S에 제근하기 위해 재공되는 Token으로 K8S 자원인 Service account에 Mapping되어 있는 Token입니다. Service Account Token은 K8S가 생성해 주는 Token으로 K8S의 SA key로 전자서명되어 있습니다.
오래 동안 기다려 왔네요. 드디어 종착지에 왔습니다. ID Token은 JWT Token중 특정한 규격을 따르는 Token 입니다. OAuth2 라는 Authorization을 위한 규격이 있습니다. 이는 쉽게 생각하면 제 3자 권한 대행으로, 동일 가입자가 A와 B 두 서비스에 동시에 가입했을 떄, A Service에서 B Service를 사용할 수 있게 하기 위한 기술 입니다. 이때 Access Token이라는 JWT Token을 Bearer Token으로 사용합니다.
이 OAuth2 구조위에, Authentication로 추가 한 것이, OIDC(Open ID Connect)라는 기술입니다. Wavve를 T맴버쉽 ID로도 로그인 하는 경우 사용되는 기술로 이해하면 됩니다. 이때 사용되는 JWT Token 형식이 ID Token 입니다.
K8S는 OIDC 규격 기준으로 Authentication를 3rd Party Solution에 위임할 수 있습니다.
아주 간단하게 요약하면 아래 그림으로 설명 가능합니다. 즉 사용자/그룹을 관리하는 별도 시스템을 외부에 두고, Kube API Server는 외부 시스템을 통해 Authentication을 실시 하는 것 입니다.
Open ID Connect Tokens 라는 K8S 공식 문서에 자세한 절차는 명시되어 있습니다.
API Server는 ID Token에 포함된 특정 Claim을 User와 Group에 매핑 할 수 있는 설정을 통해, 인증된 사용자의 Authorization / Admission 을 실시 합니다.
Authorization은 etcd에 저장된 특정 자원에 대한 CURD에 대한 권한제어를 의미 합니다.
K8S는 4가지 Authorization 을 제공 합니다.
Kubelet의 권한에 대한 건으로 특별한 권한에 대한 제어 입니다.. 사용자에 관련된 것이 아닙니다.
OIDC 연결을 통해 인증을 했듯이, 권한도 3rd Party Solution에 위임하는 방식으로, 인증시 사용된 Token과 API request를 외부 Solution에 전달하여 특정 자원에 대한 권한 적용 여부를 결정하는 방식으로, 매우 Flexible하나 K8S Cluster별 사용되는 자원의 종류 및 생성된 자원등에 대한 정보를 항상 Sync. 시켜야 하기 때문에 유지보수 비용이 많이 듭니다.
Kube API Server 기동 시, Authorization 관련 설정 파일을 Argument로 읽어서 권한 설정을 하는 방식 입니다. Plain Text Token과 유사한 Framework 입니다. 따라서 구조적으로 같은 단점을 갖습니다. 다만 Authorization 설정파일을 다양한 방식으로 변경가능(e.g. 시간이나 위치에 따라 다른 설정) 하지만 그 효용성에 대해서 의문입니다. 아래는 권한 파일의 예입니다. 판단은 각자 알아서 하시길 바랍니다.
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"*", "nonResourcePath": "*", "readonly": true}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"system:controller_manager", "namespace": "*", "resource": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"kubelet", "namespace": "*", "resource": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"kube_proxy", "namespace": "*", "resource": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"system:logging", "namespace": "*", "resource": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"system:monitoring", "namespace": "*", "resource": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"system:serviceaccount:kube-system:default", "namespace": "*", "resource": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"alice", "namespace": "alice-space", "resource": "*", "apiGroup": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"bob", "namespace": "alice-space", "resource": "*", "apiGroup": "*", "readonly": true }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"d6860ebb-b2f5-4eca-8ac6-3acd1cc17356", "namespace": "shared-cluster-partition-test", "resource": "pods", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"d6860ebb-b2f5-4eca-8ac6-3acd1cc17356", "namespace": "shared-cluster-partition-test", "resource": "namespace", "apiGroup": "*", "readonly": true}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"d6860ebb-b2f5-4eca-8ac6-3acd1cc17356", "namespace": "shared-cluster-partition-test", "resource": "quota", "apiGroup": "*", "readonly": true}}탐험에서 언급된 방식 입니다. Role 이란 Kubernete API에 대한 권한 Set 입니다. Role Binding은 특정 Role(권한)을 Kube API Server를 사용하는 대상에 제공하는 것으로, Kube APi Server를 사용하는 대상은 SW와 사람으로 나눌 수 있고, SW는 Service Account라는 K8S 자원으로 추상화 되면, 사람은 User와 Group 으로 추상화 됩니다.
RBAC을 위한 설정은 etcd에 저장되는 K8S Resource로 K8S API를 통해 관리가 가능합니다. 따라서 사용자 관리를 위해서는 RBAC를 사용하는 것을 필자는 권고합니다.
Admission은 인증과 인가를 거친 후, 실제 Resource의 CRUD를 실행하기 전에 Fine grain 제어를 위해 사용됩니다. RBAC의 경우 REST API에 대한 권한으로 Resource 에 대한 CRUD에 대한 권한을 제어 합니다. 하지만 실재 업무에서는 Resource의 특정 Field를(e.g annotation)을 관리 측면에서 설정하지 않으면 실행 하지 못 하게 한다던가, audit을 위해 특정 annotation을 추가하는(mutation) 기능들이 요구 되기도 합니다. 이런 부분은 RBAC으로는 제공하기 어렵습니다. K8S는 Policy라 자원으로 이를 지원합니다. 또한 Extended Policy를 제공하기 위해 Admission Webhook을 통해 OPA Gatekeeper와 같은 외부 솔류션과 연동하기도 합니다. Admission은 받아들이게 따라서 좀더 정교한 RBAC이라 생각 될 수도 있으나 다른 개념입니다.
Admission 에 대해서는 다른 기고 에서 더 자세히 다루고자 합니다.
사용자관리 기능을 제공하기 위해서는 K8S Cluster외 외부 Solution을 개발해야 하고, SW maintenance를 고려하면 최대한 검증되고 공개된 기술을 사용해야 합니다. 따라서 아래와 같은 시스템을 구축하는 것을 권고 합니다.
Conceptual Architecture 구성을 위해, Open Source를 활용하여, 전체 Framework을 구축해 봅시다.
다음과 같은 시나리오를 가정해 봅시다. 어떤 K8S Cluster에는 boss와 bob 이라는 User가 있습니다. boss와 bob은 각각 아래와 같은 group에 속해 있고, 각 group은 다음과 같은 권한을 가지고 있습니다.
admin : k8s 관리자로 모든 권한을 가지고 있다.
LeastPriviliges : k8s에 어떤 namespace가 존재하고, 어떤 node가 붙어 있는지 확인만 해 볼 수 있다.
foo : foo 라는 namespace에 대한 접근 권한만 가지고 있다.
User와 Group에 대한 관리는 IAM system(Keycloak)이 관리합니다. 또한 OIDC 연동을 통해 사용자는 Keycloak을 통해 ID Token을 획득하여 Authentication 을 수행하고, Authorization은 K8S RBAC의 의해서 제어됩니다.
시나리오의 권한을 K8S RBAC에 대응해 보겠습니다.
admin과 LeastPriviliges는 cluster level의 권한 입니다. 따라서 ClusterRole과 ClutserRoleBinding 생성이 필요합니다.
Admin의 경우, cluster-admins라는 K8S가 미리 재공하는 ClusterRole을 활용합니다.
## Admin Group을 위한 K8S RBAC 설정
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: devocean:admin #admin 이란 이름이 일반적이라서 prefix를 붙였습니다.
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: devocean:admin #admin 이 일반적이라서 prefix 붙임 LeastPriviliges는 당 시나리오에서 만들 Role이 필요함으로 ClusterRole과 ClutsterRoleBinindg을 모두 생성해야 합니다.
## LeastPriviliges를 위한 K8S RBAC 설정
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: devocean:leastpriviliges
rules: # 권한은 namespace와 node 자원에 대한 read
- apiGroups:
- '*'
resources:
- namespaces
- nodes
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: devocean:leastpriviliges
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: devocean:leastpriviliges
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: devocean:leastpriviligesfoo는 특정 namespace에 종속되는 group입니다. 따라서 Role과 Rolebinding 생성이 필요합니다. 따라서 foo라는 namespace를 만든 후, 아래 RBAC을 설정합니다.
## foo Group을 위한 K8S RBAC 설정
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: devocean:foo
namespace: foo
rules:
- apiGroups:
- '*'
resources:
- '*'
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: devocean:foo
namespace: foo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: devocean:foo
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: devocean:fooKeycloak은 가장 대표적인 IAM(Identity and Access Management) System 입니다. 또한 OIDC를 지원하는 대표적인 Platform 입니다. 보통 Keycloak 설치에 대한 많은 글 들은 Quick Start 수준의 동작 자체만을 확인하거나 OIDC 동작을 공부하기 위한 목적으로 쓰여져 있습니다. 본 기고에서는 그래도 어느정도 상용에 적용 할 수 있는 HA 구성의 Keycloak을 구성해 보기로 합니다.
본 기고에서는 Keycloak이 Muti-Site HA 구성에 사용하는 Active-passive 구성을 적용하여 K8S위에 Keycloak을 구축하려고 합니다. 필자의 판단으로는 Actvice-passive 구성이 K8S에 Keycloak을 HA로 구성하기 제일 적합한 구성이라고 판단했기 때문입니다. 사전 요구 시스템은 K8S 외부에 설치되어 있는DB (e.g. postgreSQL)입니다.
Active-passive 구성은 아래와 같습니다
Keycloak은 일반적인 3 Tier Architecture를 가지고 있습니다. 또한 DB Access에 대한 속도 저하와 Keycloak Cluster구성시 Data consistency를 확보하기 위해서, Infinispan 이라는 분산 Cache Layer를 사용합니다.
Active-passive 구성에서는 모든 Keycloak Instance는 독립적인 Active 상태로 Infinispan간 P2P로 모든 정보를 공유하여 사용 합니다. 참고로 Active-standby 구성은 Keycloak Instance마다 Actvie혹은 Standby role이 부과 되야 하기 때문에, Active를 선출하는 과정이 필요하고, 필연적으로 Cluster를 구성하는 Instance간 강한 연동이 필요합니다. 이에 반해 Active-passive구성은 Keycloak Instance들 자체는 홀로 독립적으로 동작하는 형태이고 다만 Cach coherenet는 Infinispan이라는 독립적은 system에 의해 확보되며, Infinispan의 option도 ad-hoc 이라는 완전 분산 방식을 채택하기 때문에, 이는 K8S의 Pod Scaling구성과도 잘 Match 됩니다.
Infinispan은 Java에서 제공하는 JGRUOP를 통해 Cluster에 속한 Infinispan과 안정적인 Data공유를 위한 통신채널을 제공받습니다.
일반적으로 Clustering(여러개의 Instance를 묶어 하나로 보이게하는 구조)을 위해서는 통상 아래와 같은 두개의 절차가 필요합니다.
Neighbor discovery : Cluster에 참여할 이웃(내가 아닌) 컴퓨터를 탐색하는 과정
Group communication : Cluster에 속한 이웃들에게 Multi-cast형태로 안정적으로 정보를 전달하는 과정
통상 Broadcast Channel을 통한 Neighbor discovery 후, Unicast channel을 통해 Cluster Join 후, Unicast channel을 통한 Group communication을 실시하는 경우가 많이 있습니다.
하지만, 일반적인 가상화 환경이나 CNI중 Overlay network인 경우 Broadcast channel를 제공하지 않는 경우가 많습니다. 또한 Broadcast 의 경우, Network이 분리된 Multi-site 구성에서는 사용할 수 없습니다.
따라서 통상 TCP Broadcast란 기상천외한 방식이 사용되기도 합니다. TCP Multicast라고 통상 불려지는 이 동작은 다음과 같은 동작입니다.
가장 대표적인 구현방법은 DNS를 활용하는 방법입니다. Cluster에 참가하는 Node들끼리는 미리 FQDN을 공유 합니다(e.g. group-a.foo.com). Cluster에 참여할 Node들은 DNS에 약속한 FQDN과 Node IP를 각자 등록합니다. 이후 Cluster에 속한 Node에 Data를 보내고 싶은 Node는 Node들에 할당된 FQDN(e.g. group-a.foo.com)에 대한 DNS Query를 통해 Cluster에 속한 Node들을 알아낼 수 있습니다.
하지만 이런방식의 경우, DNS라는 Infra에 접근 권한을 어떻게 안전하게 배포하는 문제를 해결해야 합니다. 예상하셨듯이 Infra에 대한 관리는 Infra가 설치된 환경 및 운영하는 조직의 성격등에 따라서 매우 다양해 집니다. 따라서 통산의 Framework들은 DNS Query를 통해 IP 주소 획득 후, 그룹 Communication을 어떻게 안정적으로 제공하는지에 대해서 고민하고 DNS에 IP를 등록하는 기능이 out of scope으로 관리하는 경우가 많습니다.
Headless Service를 이해하기 전에, K8S Service를 이해해야 합니다. Service는 기계적으로 말하면 다수의 Homogeneous pod들로 구성된 응용에 단일 End-point를 제공하는 것 입니다. 더 간단하게 이야기 하면 Load balancer를 제공하는 것 입니다.
위 그림과 같이 대표 IP (192.168.10.100)을 부여하는 것 이고, 이 대표 IP를 Head라고 생각합니다. 대표IP는 Kubernetes가 자동으로 할당해 줍니다.
Headless Service는 이 대표 IP를 자동으로 할당해 주지 않습니다. 따라서 bob.foo.svc.cluster.local과 같은 DNS query시 Service와 연결된 pod의 IP List를 응답합니다. Headless Service는 K8S가 제공하는 Service분배 알고리즘 대신, 다른 방법을 사용하기 위해서 많이 활용됩니다. 정확히 TCP Multicast를 구성하기 위해 누군가 해 줘야 할 일을 K8S은 Headless Service를 생성하는 것 만으로 쉽게 완료할 수 있습니다. 매우 다행이라고 생각됩니다.^^
이젠 K8S Cluster와 PostgreSQL이 설치 된 환경만(?!) 구축해 주세요. Keycloak의 구성은 간단합니다.
미리 확보해 놓아야 할 List 입니다.
DB의 Admin ID/PW 확보
DB에 Keycloak 이란 이름의 Table 생성
DB와 Keycloak이 사용한 FQDN 확보 (e.g. postgre.foo.com , keycloak.foo.com)
Keycloak은 HTTPS 통신을 제공해야 함으로 인증서확보
아래 manifast는 keycloak이라는 namespace에 설치됨을 가정함 keycloak namesapce 생성
apiVersion: v1
kind: Service
metadata:
name: keycloak
labels:
app: keycloak
spec:
ports:
- name: http
port: 8080
targetPort: 8080
- name: infinispan
port: 7800
targetPort: 7800
protocol: TCP
selector:
app: keycloak
---
## headless Service
apiVersion: v1
kind: Service
metadata:
name: infinispan
spec:
clusterIP: None ## headless Service 생성 방법
selector:
app: keycloak
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
replicas: 2
strategy:
type: "RollingUpdate"
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:21.0.1
args: ["start"]
env:
- name: KC_HOSTNAME
value: "keycloak.foo.com" ## Global Access 가능한 DNS Name
- name: KC_HTTP_RELATIVE_PATH
value: "/auth" ## Admin console에 login하려면 설정 필요
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_DB
value: "postgres"
- name: KC_DB_PASSWORD
value: "1q2w3e4r"
- name: KC_DB_URL_HOST
value: "postgre.foo.com"
- name: KC_DB_URL_PORT
value: "5432"
- name: KC_DB_USERNAME
value: "postgres"
- name: KEYCLOAK_ADMIN
value: "admin"
- name: KEYCLOAK_ADMIN_PASSWORD
value: "admin"
- name: KC_PROXY
value: "edge" ## reverse proxy 뒷단에 KC가 존재한다는 의미
- name: KC_CACHE_STACK
value: "kubernetes"
- name: KC_TRANSACTION_XA_ENABLED ## 2PC 지원여부. DB Clustering에 맞김
value: "false"
- name: JAVA_OPTS
value: "-Djgroups.dns.query=infinispan.keycloak.svc.cluster.local" ## TCP multicast를 위한 DNS 주소
ports:
- name: http
containerPort: 8080
- name: infinispan
containerPort: 7800
readinessProbe:
httpGet:
path: /auth/health
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
spec:
ingressClassName: nginx ## nginx ingress controller 가정
rules:
- host: keycloak.foo.com
http:
paths:
- path: /auth
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
tls:
- hosts:
- keycloak.foo.com
secretName: keycloak-tls생성된 Keycloak의 admin password는 아래와 같은 명령어로 알 수 있습니다.
kubectl get secret keycloak -n keycloak -o jsonpath={.data.admin-password} | base64 -dkeycloak.foo.com/auth 를 부라우저에 입력하면 keycloak이 성공적으로 동작하는 것을 확일 할 수 있을 것 입니다.
Keycloak에 적절한 설정을 위해서는 Keycloak과 OIDC를 모두 이해해야 합니다. 이를 위한 간단한 Diagram을 그려서 진행해 보겠습니다.
Keycloak은 ID Token을 제공하는 서버입니다. ID Token은 앞서 이야기 한 듯이 결국, Token 사용자의 자격을 인증하기 위한 수단입니다. 여기서 자격은 Claim(주장)이라고 합니다. 따라서 Keycloak은 내부에 저장하고 있는 사용자의 대한 정보(e.g username, group, email address 등)에서 Client가 자신을 증명하기 위해 필요한 Claim에 대응되는 값을 Mapping해서 전달해야 합니다. 따라서 Client 종류별 필요한 설정을 미리 해야 합니다.
또한 Keycloak은 Multi-tenant를 지원하고 Tenant별로 독립된 사용자 관리가 가능합니다. 이를 Realm이라 부릅니다.
따라서, Kubernetes RBAC에 적용하기 위한 Claim과 해당 Claim을 제공하기 위해 Keycloak의 어떤 내부 사용자 정보를 사용해야 하는지에 대한 설계가 필요합니다. 특히 어떤 Keycloak의 내부 정보를 ID Token의 claim으로 사용 할지에 대해서는 수많은 설계가 나올 수 있습니다.
본 기고에서는 필자가 보기에 직관적이란 판단되는 방법에 의해서 설계 후, 해당 설계대로 Keycloak을 설정 하는 방법을 기술 하겠습니다.
Keycloak의 UX를 제공합니다. 많은 기고들이 이 UX 기준으로 설명을 합니다. 하지만 본 기고에서는 REST API 기준으로 설명하겠습니다. 그 이유는 향후 독자들이 SW 개발을 통해 시스템 구축 시 조금이나마 도움이 되길 바라기 떄문입니다.
참고
문 기고는 아래 REST API 기준으로 설멍합니다.
https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html간략하게 Keycloak 설정을 도식화 하면 다음과 같습니다. 우선 Realm을 생성하고, Realm에 사용할 User와 Group을 생성합니다. 서비스 시나리오와 같이 boss와 bob 이라는 사용자와 devocean:admin, devocean:leastpriviliges, devocean:foo라는 group을 만들고, boss에는 devocean:admin group를 할당하고, bob에는 devocean:leastpriviliges와 devocean:foo를 할당합니다.
그 후, OIDC설정을 위해서 ID token에 prefered_username과 groups를 cliam으로 전달 할 수 있게 Keycloak Client를 설정합니다. Client는 Keycloak의 자원으로 앞서 설명한 client config 입니다.
Keycloak admin REST API를 접근하기 위해서는, Access Token이 필요합니다.
이 예제는 아래와 같은 가정을 바탕으로 합니다.
Keycloak Endpoint : https:/keycloak.foo.com/auth
Admin ID/PW : admin / password
curl / jq 등 필요한 tool 은 사전 설치되어 있어야 합니다
아래 Shell 명령어로 TOKEN이란 환경변수에 Access Token을 저장합니다.
export TOKEN=$(curl -k -X POST https://keycloak.foo.com/auth/realms/master/protocol/openid-connect/token -d grant_type=password -d username=admin -d password=password -d client_id=admin-cli | jq -r '.access_token')Project A를 지칭하는 pro-a 라는 Realm을 생성한다.
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"realm":"org-a", "enabled":"true"}' https://keycloak.foo.com/auth/admin/realmsrealm org-a 에 Group 생성
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"name" : "devocean:admin"}' https://keycloak.foo.com/auth/admin/realms/org-a/groups
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"name" : "devocean:leastpriviliges"}' https://keycloak.foo.com/auth/admin/realms/org-a/groups
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"name" : "devocean:foo"}' https://keycloak.foo.com/auth/admin/realms/org-a/groupsUser 생성
#boss.json
{
"username": "boss",
"enabled": "true",
"credentials": [
{
"type": "password",
"value": "qwer",
"temporary": false ## 다음 로그인시, 비번 초기화 안됨
}
]
}
#bob.json
{
"username": "bob",
"enabled": "true",
"credentials": [
{
"type": "password",
"value": "qwer",
"temporary": false
}
]
}realm org-a 에 User 생성
curl -v -k -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d @boss.json https://keycloak.foo.ocm/auth/admin/realms/org-a/users
curl -v -k -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d @bob.json https://keycloak.foo.com/auth/admin/realms/org-a/usersUser에 Group Mapping
#get User ID
export BOSS=$(curl -v -k -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keyclaok.foo.com/auth/admin/realms/org-a/users?username\=boss | jq -r .[0].id)
export BOB=$(curl -v -k -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keyclaok.foo.com/auth/admin/realms/org-a/users?username\=bob | jq -r .[0].id)
#get Group ID
export K8SADMIN=$(curl -v -k -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keyclaok.foo.com/auth/admin/realms/org-a/groups?search\=devocean:admin | jq -r .[0].id)
export K8SUSER=$(curl -v -k -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keyclaok.foo.com/auth/admin/realms/org-a/groups?search\=devocean:priviliges | jq -r .[0].id)
export K8SFOO=$(curl -v -k -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keyclaok.foo.com/auth/admin/realms/org-a/groups?search\=devocean:foo | jq -r .[0].id)
# User 'boss'에 'decocean:admin' group 할당
curl -v -k -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/users/$BOSS/groups/$K8SADMIN
# User 'bob'에 'k8s:foo' group , 'k8s:user" group 할당
curl -v -k -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/users/$BOB/groups/$K8SUSER
curl -v -k -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/users/$BOB/groups/$K8SFOOKeycloak Client 생성
##client-k8s.json
{
"clientId" : "k8s",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled":true, ## REST API를 통한 직접 접근. Token 검증등
"protocol": "openid-connect"
}REST API
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d @client.json https://keycloak.foo.com/auth/admin/realms/org-a/clientsKeycloak은 다양한 mapper를 구성할 수 있습니다. 하나의 Realm(가입자DB)에 여러가지 Client가 Token을 요구 할 수도 있고, 각 Client가 사용하는 서비스 (e,g K8S, Grafana)는 각기 다른 RBAC을 제공 할 수 있습니다. 따라서 Client별로 독립적인 mapper 설정을 갖을 수 있습니다. Keycloak은 client내 client-scope이라는 자원을 복수로 가질 수 있고 이 client-scope에 mapper를 포함하여, 상황에 맞게 scope별 claim을 선택적으로 제공 할 수 있습니다.
client-scope 생성
언급한 2개의 mapper를 설정 합니다. 필요로한 최소한의 claim만 ID Token에 적용하게 설정했습니다.
#clientscope.json
{
"name" : "oidc" ,
"protocol" : "openid-connect",
"protocolMappers" : [
{
"name" : "groups" ,
"protocol" : "openid-connect",
"protocolMapper" : "oidc-group-membership-mapper",
"consentRequired" : "false" ,
"config": {
"full.path":"false",
"id.token.claim":"true",
"access.token.claim":"false",
"claim.name":"groups",
"userinfo.token.claim":"false"
}
},
{
"name" : "user" ,
"protocol" : "openid-connect",
"protocolMapper" : "oidc-usermodel-property-mapper",
"consentRequired": "false" ,
"config": {
"user.attribute" : "username",
"full.path":"false",
"id.token.claim":"true",
"access.token.claim":"false",
"claim.name":"preferred_username",
"userinfo.token.claim":"false"
}
}
]
}REST API
curl -v -k POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d @clientscope.json https://keycloak.foo.com/auth/admin/realms/org-a/client-scopesClient에 Cleint-scope 등록
default-scope으로 등록하여, 항상 oidc 란 scope에 정의된 claim 제공함
#Get Client-Scope ID
export SCOPE=$(curl -v -k GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/client-scopes | jq -r '.[]|select(.name=="oidc").id')
#Get Client ID
export CLIENT=$(curl -v -k GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/clients?clientId\=k8s | jq -r .[0].id)
#client scope oidc를 k8s client의 default scope으로 등록하기
curl -v -k -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" https://keycloak.foo.com/auth/admin/realms/org-a/clients/$CLIENT/default-client-scopes/$SCOPE아래와 같은 cli를 통해 token을 확인 할 수 있습니다. (OIIDC 기준으로 Token을 달라는 명령어입니다.)
curl -k -X POST https://keycloak.foo.com/auth/realms/org-a/protocol/openid-connect/token -d grant_type=password -d username=bob -d password=qwer -d client_id=k8s -d scope=openid | jq -r .id_token이재 Kube API Server가 특정 ID Token을 Bearer Token형태로 가지고 API Call을 한 Response에 대해서 Token 거증을 하기 위해, OIDC Token verification에 대한 서버 연결 설정을 해야 합니다. control plane이 동작하는 node의 kube-apiserver 설정파일에 아래와 같이 OIDC 설정 부분을 추가해 줍니다.
## /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
..
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
... ## OIDC 설정
- --oidc-issuer-url=https://keycloak.foo.com/auth/realms/org-a
- --oidc-client-id=k8s
- --oidc-username-claim=preferred_username
- --oidc-groups-claim=groups
...이제 Kube API Server로 접근 신, Bearer Token으로 ID Token을 발급받아 사용하면 됩니다. 하지만 사용자는 Kubectl 이라는 CLI Tool을 이용해서, K8S에 접근합니다. 앞서 살펴본 것과 같이 Kube API Server는 Token, Client X.509 인증서 모두를 지원합니다. 따라서 kubeconfig 파일에 client 인증서를 user spec에 넣어서 사용해 왔습니다.
K8S는 AuthN에 대해서 외부 시스템 연동을 지원하기 때문에, kubectl CLI입장에서도 이를 활용 할 수 있는 장치를 제공합니다.
Client Authentication 란 Spec 입니다. 이를 통해서 Kubeconfig내 user에 대한 증명을 Kubectl plug-in을 통해 동적으로 확보하여 사용 살 수 있습니다.
kubectl은 plug-in을 제공하여, sub commend를 확장할 수 있는 수단을 제공합니다. 이는 Extend kubectl with plugins 란 문서로 K8S 공식 문서에 소개되고 있습니다.
kubectl plugin 에 대해서는 Devocen의 아티클인 AKS로 쿠버네티스 시작하기 : kubectl plugin 에 자세히 다루고 있으나 참조 부탁드립니다.
kubelogin은 kubectl plugin 중 하나로, oidc-login이란 sub commend를 제공합니다. 이를 통해 ID Token을 발급받을 수 있습니다.
kubelogin은 Open Source지만 Azure에서 관리하는 SW 입니다. Public Cloud 사용 시, Public Cloud용 CLI를 샐행 시, Web Browser가 자동으로 뜨면서 ID/PW를 입력하는 절차가 진행되는 경우를 본 적 있을 것 입니다. 이떄 상요되는 기술 입니다.
자세한 내용과 설치는 Github 공식 Page를 통해 실행해 주십시요.
다음과 같은 명령을 통해, 동작을 확인 할 수 있습니다. 아래 명령어는 ID Token을 획득 하는 것 입니다.
kubectl oidc-login get-token --oidc-issuer-url=https://keycloak.foo.com/auth/realms/org-a --oidc-client-id=k8s --username=bob --password=qwer이미 앞서서 Token을 받는 Plug-in을 확인했습니다. 이 명령어를 활용하여 Client Authentication Spec을 kubeconfig 파일을 생성합니다.
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: [당신은 K8S CA]
server: https://[당신의 Kube API Server Endpoint. eg, boo.foo.com:6443]
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: oidc
name: oidc-user@kubernetes
current-context: oidc-user@kubernetes
kind: Config
preferences: {}
users:
- name: oidc
user:
exec: ## User의 인증을 아래 설정에 따라 받아 온다는 설정입니다.
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://keycloak.foo.com/auth/realms/org-a
- --oidc-client-id=k8s
- --grant-type=password 자 잘되는지 확인 봅시다. ID/PW 를 입력 하라고 CLI가 나옵니다 bob으로 로그인합니다.
$ kubectl get ns
Username: bob
Password:
NAME STATUS AGE
calico-apiserver Active 18d
calico-system Active 18d
default Active 18d
foo Active 14d
kube-node-lease Active 18d
kube-public Active 18d
kube-system Active 18d
tigera-operator Active 18dbob은 foo ns외 다른 ns에 대해서는 권한이 없습니다. 잘 되는지 확인해 봅시다.
$ kubectl get pod -n kube-system
Error from server (Forbidden): pods is forbidden: User "https://keycloak.foo.com/auth/realms/org-a#bob" cannot list resource "pods" in API group "" in the namespace "kube-system"그럼 boss로는 잘 되는지 알아 보겠습니다. 하지만 log-out은 어떻게 하는 것 일까요? 아쉽게도 현재로는 log-out은 명시적으로 제공되지 않고, cache를 지우는 방법을 써야 합니다.
$ cd
$ rm .kube/cache/oidc-login/*똑같은 일을 boss로 로그인 해서 실시해 보겠습니다.
$ kubectl get pod -n kube-system
Username: boss
Password:
NAME READY STATUS RESTARTS AGE
coredns-76f75df574-kqwnj 1/1 Running 9 (4h21m ago) 18d
coredns-76f75df574-r9ng7 1/1 Running 9 (4h21m ago) 18d
etcd-ip-172-31-105-199 1/1 Running 9 (4h21m ago) 18d
kube-apiserver-ip-172-31-105-199 1/1 Running 4 (4h21m ago) 11d
kube-controller-manager-ip-172-31-105-199 1/1 Running 10 (4h21m ago) 18d
kube-proxy-cmr9z 1/1 Running 9 (4h21m ago) 18d
kube-scheduler-ip-172-31-105-199 1/1 Running 10 (4h21m ago) 18d긴 글이 끝났네요. 의도와 설계 배경 그리고 간단한 실습을 통한 실무적용까지 가능하게 글을 쓰려고 노력 중 입니다.
부디 누군가에 도움이 되었으면 합니다.
끝 -
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.