kubectl sso with keycloak 삽질 일기

KEUN·2022년 3월 20일
3

최근 팀에서는 보안심사 준비 등을 이유로 미뤄두었던 Network 및 User 접근 제어에 신경쓰고있다.

이전 직장은 비교적 큰 규모의 조직이었기에 시스템 엔지니어링 팀이 작은 부분까지 잘 신경을 써주었지만,

비교적 소규모 현재 조직은 별도로 시스템 엔지니어링 팀은 없고 backend 팀에 인프라 리소스를 전담해줄 인력이 부족한 상황이라 부족한 것이 많다.

OS User, kubernetes User 관리 등 여러가지 개선 과제가 있지만, 이번 삽질 일기에서는 kubernetes User의 접근 제어를 설정하게 되면서 마주쳤던 것을 써보려한다.

kubectl sso with keycloak

이제까지는 단 하나의 kubectl config를 공유해서 kuberenets에 접근해왔기 때문에 User 인증에 대한 통제는 잘 이루어지고 있지 않았다.

kubenetes에서는 User를 나타내는 객체는 따로 존재하지 않고, 실제로는 Service Account Token, OIDC, CA 등 어떤 방식으로 Authentication을 통과하느냐에 대한 문제라고 생각하는데,

현재 팀에서는 single sign-on을 위해 keycloak 이라는 오픈소스의 sso software를 사용하고 있었기 때문에 OIDC 인증을 활용해 User 인증을 통제하고자 했다.

Authentication

이와 관련해서 여러가지 keycloak 사용 사례를 찾던 와중, kubernetes korea group에 소개되었던 영상이 있어 이를 참고해 keycloak를 통한 User 인증 통제를 구현했다.

Keycloak을 이용한 Kubernetes 사용자 분리방법 - 이재상
https://www.youtube.com/watch?v=owaK-V2MAjU

위 영상을 한번 살펴보면 금방 이해가 되겠지만, 전체 설정 과정을 요약하자면 아래와 같다.

  1. keycloak client를 생성하고 필요한 protocol mapper 생성
    1. protocol mapper를 통해 access token에 group 정보를 주입
  2. kubernetes cluster에 OIDC Identity Providers로 keycloak 추가
  3. kubectl 설정을 keycloak으로부터 토큰을 받아 API 요청을 하도록 수정

keycloak client와 protocol mapper란?

  • Clients are entities that can request Keycloak to authenticate a user.
  • Protocol mappers map items (such as an email address, for example) to a specific claim in the identity and access token.

이렇게 설정이 완료되면 kubernetes의 User Authentication을 위해 keycloak으로 부터 토큰 발급이 필요하게 된다.

이때 각자의 keycloak account를 사용하면 되니 자연스럽게 개인별로 User 분리가 가능하게 된다.

Authorization

User 접근 제어는 계정 분리 뿐만 아니라 알맞은 권한의 부여도 목적이다.

kubernetes 역시 Authentication를 통과한다고 해서 API 요청을 허락해주지 않는다.
아래의 그림 처럼 Authentication이 통과되었으니 이제 권한을 확인하기 위해 Authorization 과정도 통과해야한다.

https://kubernetes.io/ko/docs/concepts/security/controlling-access/

Role 정의

Kubernetes는 Role-based-access-control(RBAC)을 위한 role, role-binding 과 같은 Object를 제공하니 그것을 사용해 적절한 권한을 만들어주면 되겠다.

이때 role과 role-binding에 따라 유효한 scope이 있기 때문에 아래와 같이 type에 따라 사용될 수 있는 경우를 생각해서 role을 만들어주었다.

AccessRole TypeBinding Type
클러스터 수준 리소스(node,pv 등)clusterroleclusterrolebinding
비 리소스 URL (/api,/healthz 등)clusterroleclusterrolebinding
여러 네임스페이스에 있는 리소스
그리고 모든 네임스페이스에 걸쳐있는 리소스
clusterroleclusterrolebinding
특정 네임스페이스 있는 네임스페이스 리소스
다수의 네임스페이스에 동일한 클러스터롤 재사용하는 경우
clusterrolerolebinding
특정 네임스페이스 있는 네임스페이스 리소스
롤은 각 네임스페이스에서 정의되어야 한다.
rolerolebinding

Role 부여

만들어둔 role을 User에게 연결(binding)해줘야 한다.

User sso 인증을 위해서 keycloak client를 설정하는 과정에서 요청된 access token에 대해 group 정보를 mapping 해주도록 설정해주기도 했고,

Group based로 접근 제어를 하는 것이 role 생성/관리 차원에서도 편하기 때문에 role-binding은 subject를 Group을 대상으로 하도록 했다.

keycloak role과 kubernetes의 group

이렇게 keycloak을 통해 User Authentication을 처리하고, Authentication과 통과된 유저는 자신에게 부여된 Role에 따라 Authorization도 통과되어 리소스에 접근할 수 있게 되었다.

근데 이게 왜 가능한 걸까?

정확히는 keycloak으로 부터 받은 token을 통해 Authentication는 통과했다지만, kubernetes는 뭘 보고 Authorization까지 처리해준걸까? User가 어떤 Group인지 어떻게 알았을까?

이것은 앞서 설정한 keycloak client의 protocol mapper에 답이 있었다.

protocol mapper를 설정할 때 keycloak role을 Token 내부에 Group이라는 이름으로 정보를 주입시키도록 설정해두었기 때문에 kubernetes는 제출받은 Token에서 Group 정보를 확인하고 인가 처리까지 해주는 것이다.

아래는 실제 Keycloak으로 받은 Token을 jwt.io를 통해 decode한 내용인데, Token 정보 중, Groups라는 key가 존재하는 것을 알 수 있다.

{
  "jti": "***,
  "exp": ***,
 (생략)
  "name": "LEE GEUNJE",
  "groups": [
    "developer"
  ],
  "preferred_username": "foobargem@foobar.com",
  "given_name": "LEE",
  "family_name": "GEUNJE",
  "email": "foobargem@foobar.com"
}

oidc authenticator: initializing plugin: 403 Forbidden

모든 설정이 완료된 후 평화로운 날이 계속되던 어느날, kubernetes 리소스를 확인하려던 나는 갑자기 아래와 같은 에러를 마주하게 되었다.

$ kubectl get pod
error: You must be logged in to the server (Unauthorized)

이게 무슨 일인가 싶었다. 분명 그동안 설정 변경은 없었는데...
분명 어제까지만 해도 잘 되던게 왜 갑자기 이러지...

답답한 마음에 api서버 로그를 살펴보니 몇몇 로그가 눈에 띄었다.

E0308 08:02:34.802539 10 authentication.go:53] Unable to authenticate the request due to an error: [invalid bearer token, oidc: authenticator not initialized, unknown]
E0308 08:02:34.802539 10 authentication.go:53] Unable to authenticate the request due to an error: [invalid bearer token, oidc: authenticator not initialized, unknown]
E0308 08:00:25.549378 10 oidc.go:224] oidc authenticator: initializing plugin: 403 Forbidden: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>

어.... 403 Forbidden? authenticator not initialized?

가만 생각해보니 얼마전 특정 pod의 리소스 사용이 급격하게 늘어 확인하던 중, 특정 pod에서 코인이 채굴되고 있는 것을 발견한 적이 있어 후속 조치로 all open(0.0.0.0/0)해두었던 keycloak alb에 제한을 걸었던 것이 생각났다.

Gitlab CPU Utilization is HIGH due to XMRig tool
https://github.com/sameersbn/docker-gitlab/issues/2448#issuecomment-962450231

이것 때문에 api server가 OIDC Provider 사이에 네트워크 통신 문제가 생겨 authenticator가 동작을 안하는 걸까 라는 의심이 들었다.

SSH는 되고, kubectl은 안되고.

우리 팀에서는 kubectl 뿐만 아니라 SSH 접근에서도 keycloak 인증이 사용되고 있다.

즉 bastion server에 SSH 접근을 할 때도 key를 이용해 User 인증을 하는 것이 아니라 keycloak account를 이용해 접근 제어를 하고있다.

keycloak alb 제한 조치 이후에도 ssh와 kubectl 모두 keycloak token을 통해 인증하고 있는데 왜 ssh 접근은 되고, kubectl은 안될까 라는 생각이 들었다.

아래의 EKS architecture의 내용처럼 control planeEKS managed VPC에 생성되는데 SSH와 Kubectl의 차이점이라 하면 keycloak과 통신할 경우 네트워크 경로가 다르다는 것이었다.

  • SSH
    - EC2 -> NAT(Customer VPC) -> External -> ALB -> Keycloak
  • EKS API server
    - API Server -> EKS managed VPC(NAT?) -> External -> ALB -> Keycloak

eks-architecture

ACL에 EKS managed VPC IP 추가하기

[* 2023/02/23 수정됨.]
keycloak으로 향하는 LB(ALB or NLB)를 internal로 하니 현재까지 문제없이 동작하고있다.
즉, keycloak pod로 향하는 LB를 public이 아닌 private vpc로 하여 vpc 내부 통신으로 돌렸더니 문제가 사라진 듯 보인다.

이전 나의 생각은 user로부터 oidc token을 받았을때 해당 token을 확인하기위해 EKS api server가 oidc provider에 요청을 보내고, 그 과정에서 managed vpc의 source ip가 식별이 어려우니 그것이 문제일 것이라 생각했는데 그게 또 아닌 것 같다.

그렇다면...
1. 내가 이해한 managed vpc와 customer vpc간 통신에 대해서 이해를 잘못 했던걸까?
2. oidc 인증 프로세스를 잘못 생각하고 있던걸까?

갈길이 멀어보인다.

[* 2022/04/27 수정됨.]
이 글을 적고나서까지도, 내가 생각했던 해결책이 효과가 있는 듯 했다.
하지만 이것은 내가 꼼꼼하지 않았던 탓에 잘못된 판단을 내렸던 것이 되었다.

아래의 내용처럼 kubernetes apiServer가 인증 서버로 인증 작업을 처리하기 위해서는 공개적으로 액세스 할 수 있는 환경을 필요로 했다.

이뿐만 아니라, eks workernode와의 networking blog라도 잘 살펴봤으면 좀 더 괜찮은 판단을 하지 않았을까 생각이 든다.

이에 따라 keycloak ingress는 별도로 분리하여 all open(0.0.0.0)을 하기로 했다.

그럼, 위에서 생각해본 내용대로 EKS managed VPC의 IP를 Keycloak ALB ACL에 추가하면 될 것 같았다.

근데 'EKS managed VPC 정보를 내가 어떻게 알아...'라고 한숨을 쉬던 와중, 그룹장님이 쓰윽 다가와서 'API 서버 엔드포인트'의 IP를 넣으면 되지 않을까요? 라고 하셨다.

곧장 AWS console을 열어 EKS의 API 서버 엔드포인트를 dnsutil로 쿼리해, 해당 IP를 ALB ACL에 넣어보았다.

그러니 됐다.(ㅋ...)

이렇게 뜬금없이 나의 발목을 잡던 놈이 사라졌다.

마무리

위와 같은 과정을 통해 현재는 팀의 엔지니어들이 각자의 계정을 통해 Kubernetes에 접근하고 있고, 권한 역시 잘 분리가 되어 admin이 아니고서는 특정 namespace의 리소스에만 접근하고 있다.

Kubernetes를 EKS로 접하면서 모르고 넘어갔던 부분이 많았고 현재도 알아야 할게 많은 것 같다.

이전 OpenStack을 하면서도 가장 관심을 가지지 않았던 부분이 User 인증(keystone)에 대한 개념이었는데 지금까지도 이렇게 신경을 못쓸 정도면 공부를 해야하는게 맞는데 그러질 못하고 있다.

oidc authenticator: initializing plugin: 403 Forbidden 문제에 대해서는 API 서버 엔드포인트의 IP를 가져와 ACL 처리 해주는 것이 최선의 길 일까 라는 생각이 든다.

private subnet에 cluster가 있으면서 서로간의 통신은 불필요하게 외부를 경유하는게 맞는가 라는 생각이 들기도 하고 그냥 private DNS를 따로 만들어서 ALB 하나 더 세우는 것은 어떨까 라는 의견도 있었다.
그러기에는 EKS에서 OIDC Identity Providers는 하나밖에 등록하지 못하는 것 같다.
아예 public로 돌려야할 것 같다.

이번 글 내용은 Dev 환경에서 발생한 내용이니 앞으로 Prd 환경에 대해서는 별도의 private route53를 생성해서 문제를 해결해볼까 한다.

삽질의 연속이다.

profile
Let's make something for comfortable development

4개의 댓글

comment-user-thumbnail
2022년 7월 13일

잘 읽구 갑니다 ㅎㅎ 아마 API Server Endpoint는 DNS를 조회해서도 얻을 수 있지만 ENI를 조회해서 같은 IP의 ENI를 조회해볼 수 있을겁니다.
즉, ENI 조회를 통해 API Server Endpoint IP 를 얻을 수 있을텐데, EKS 문서상으로는 최소 2개를 제공한다던데 이 IP값이 변하진 않는지 궁금하기도 하더라구요. 계속 ACL에서 IP 허용하기로 잘 사용하고 계신가요?

(근데 API Server의 ENI가 애초에 security group이 붙기 때문에 그냥 ALB에서 이 sg를 허용해줄 수도 있지 않을지..? 싶은데 제가 직접 인프라를 보진 못하고 드는 생각이라 그냥 지나가는 궁금증으로 적어봤습니다 ㅎㅎ)

1개의 답글