본문 바로가기

Technical/Cloud, Virtualization, Containers

[Kubernetes RBAC] API 연결로 확인해 보는 RBAC 설정 방법


RBAC(Role Based Access Control). 우리 말로는 "역할 기반 접근 제어" 라고들 표현하지만 사실, '역할' 보다는 '권한' 이라고 이해하는 것이 더  뜻이 잘 통한다. Kubernetes 1.6 버전부터 kubeadm 을 통한 Bootstrap 방식의 설치가 도입되면서 RBAC의 기본 설정이 이루어지게 되어 있는대, 구글링을 통해 접해 본 예전 문서들에서는 curl 을 통해 쉽게 접근 되던 API 들이, 권한이 없다는 메시지를 뿌리며 실패되는 모습을 자주 보게 되면서, 이 부분에 대해 직접 실험하고 규명해 보고자 하는 생각이 들기 시작했다.






Kubeadm 의 bootstrapping 에 의해 설정된 기본 RBAC 확인


먼저 마스터에서 아래의 명령을 실행해 보자.


# APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")

# TOKEN="$(kubectl get secret $(kubectl get secrets | grep default | cut -f1 -d ' ') -o jsonpath='{$.data.token}' | base64 --decode)"

# curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1

HTTP/1.1 200 OK

Content-Type: application/json

Date: Tue, 05 Dec 2017 06:19:22 GMT

Transfer-Encoding: chunked


{

  "kind": "APIResourceList",

  "groupVersion": "v1",

  "resources": [

    {

...

...

...

      "verbs": [

        "get",

        "patch",

        "update"

      ]

    }

  ]

}

* APISERVER 변수에는 마스터에서 실행되고 있는 apiserver의 URL 값이 저장된다

* TOKEN 변수에는 default 네임스페이스의 default-token-xxxxx 토큰의 secret key 값이 decode 되어 저장된다

* 토큰 키를 header에 실어서 api 목록을 가져오는 curl 호출을 실행해 본다



위의 결과를 보면 API 가 잘 호출되고 모든 것이 순조로운 것처럼 보인다. 그렇다면  연이어서 아래 명령을 실행해 보자.


# curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1/pods

HTTP/1.1 403 Forbidden

Content-Type: application/json

X-Content-Type-Options: nosniff

Date: Tue, 05 Dec 2017 06:28:47 GMT

Content-Length: 292


{

  "kind": "Status",

  "apiVersion": "v1",

  "metadata": {

    

  },

  "status": "Failure",

  "message": "pods is forbidden: User \"system:serviceaccount:default:default\" cannot list pods at the cluster scope",

  "reason": "Forbidden",

  "details": {

    "kind": "pods"

  },

  "code": 403

}

* Default 네임스페이스의 pod 리스트를 가져오는 API 호출을 실행한다

* Default 네임스페이스의 default 라는 사용자는 자신의 네임스페이스임에도 pod의 리스트를 볼 수 없다는 결과를 보여 준다



뭔가 이상하지 않은가? 첫 번 째 curl 실행과는 달리 pod들의 목록을 가져오는데 403 - 권한 없음 - 이라는 결과를 보여 준다. 그렇다면 특정 네임스페이스의 서비스 목록을 가져 오는 것 또한 불가능하지는 않을까?


# curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/kube-system/services

HTTP/1.1 403 Forbidden

Content-Type: application/json

X-Content-Type-Options: nosniff

Date: Tue, 05 Dec 2017 06:34:55 GMT

Content-Length: 316


{

  "kind": "Status",

  "apiVersion": "v1",

  "metadata": {

    

  },

  "status": "Failure",

  "message": "services is forbidden: User \"system:serviceaccount:default:default\" cannot list services in the namespace \"kube-system\"",

  "reason": "Forbidden",

  "details": {

    "kind": "services"

  },

  "code": 403

}


맞다! Kube-system 네임스페이스의 서비스 목록을 가져오는 것도 역시 403의 결과를 보여 준다. 즉, RBAC 기본 설정대로라면 kubectl 을 통하지 않고 apiserver 에게 직접 API를 호출해서 클러스터의 상태를 보거나 설정 변경을 통한 관리, 운영 자체가 불가능한 상황이 되는 것이다. 우리는 앞으로 이 문제를 해결하는 방법을 찾아 보아야 한다.



그 전에, 우선 한 가지만 정리해 두고 넘어가자. 위의 명령들을 조합해서 RBAC 권한 설정에 대한 확인을 해 보려면  아래의 bash script 를 실행해 보면 된다.


# vi check-apiserver-access-by-default.sh

#!/bin/bash
# For v1.8.x default policy, this 'curl' results in '403 Forbidden'
APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")
# Retrieve 'default' account's TOKEN in 'default' namespace
TOKEN="$(kubectl get secret $(kubectl get secrets | grep default | cut -f1 -d ' ') -o jsonpath='{$.data.token}' | base64 --decode)"
curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/default/services

# chmod a+x check-apiserver-access-by-default.sh



Kubernetes 는 여러 개의 ServiceAccount 를 기본으로 제공하는데, Default 네임스페이스의 default 계정(ServiiceAccount, k8s 방식의 표현은 'default:default') 이 아닌 다른 계정은 어떨까? 아래의 명령을 실행해 보자(v1.9)


# APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")

# TOKEN="$(kubectl -nkube-system get secret $(kubectl get secrets -nkube-system | grep kube-dns | cut -f1 -d ' ') -o jsonpath='{$.data.token}' | base64 --decode)"

# curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/default/services

HTTP/1.1 200 OK

Content-Type: application/json

Date: Tue, 05 Dec 2017 06:57:21 GMT

Transfer-Encoding: chunked


{

  "kind": "ServiceList",

  "apiVersion": "v1",

...

...

      "status": {

        "loadBalancer": {

          

        }

      }

    }

  ]

}



앞의 시도와는 다르게, 이번에는 kubernetes의 control-plane 인 kube-system 네임스페이스의 kube-dns 계정(kube-system:kube-dns) 의 토큰 키 값을 읽어 와서 API 호출을 시도해 보니 정상적으로 동작한(v.19)


# curl -D - --insecure --header "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/kube-system/services

HTTP/1.1 200 OK

Content-Type: application/json

Date: Tue, 05 Dec 2017 06:58:22 GMT

Transfer-Encoding: chunked


{

  "kind": "ServiceList",

  "apiVersion": "v1",

...

...

      "status": {

        "loadBalancer": {

          

        }

      }

    }

  ]

}


마찬가지로 kube-system 네임스페이스의 서비스 목록을 가져오는 것도 역시 가능하다. 그렇다면, API 호출을 통한 모든 행위(action)를 kube-system:kube-dns[각주:1] 계정으로 할 수 있는 것일까? 답은 "아니오" 이다. 맨 위의 그림이 그에 대한 설명을 보여 주고 있다. 다시 말하면 kube-system:kube-dns 계정(subject; 대상)에게는 전체 클러스터의 모든 서비스와 엔드포인트(cluster-wide, 즉 네임스페이스에 상관 없이) 들에 대해 'list', 'watch' 만 가능하도록 사전 설정 되어 있기 때문이다.



아래의 수행 결과는 현재 k8s 클러스터 내에서 위 그림의 파란 박스에 해당하는 system:kube-dns 라는 ClusterRole/ClusterRoleBinding 에 대한 설정 내용을 조회한 결과이다.


# kubectl describe clusterrole system:kube-dns

Name:         system:kube-dns

Labels:       kubernetes.io/bootstrapping=rbac-defaults

Annotations:  rbac.authorization.kubernetes.io/autoupdate=true

PolicyRule:

  Resources  Non-Resource URLs  Resource Names  Verbs

  ---------  -----------------  --------------  -----

  endpoints  []                 []              [list watch]

  services   []                 []              [list watch]


# kubectl describe clusterrolebinding system:kube-dns

Name:         system:kube-dns

Labels:       kubernetes.io/bootstrapping=rbac-defaults

Annotations:  rbac.authorization.kubernetes.io/autoupdate=true

Role:

  Kind:  ClusterRole

  Name:  system:kube-dns

Subjects:

  Kind            Name      Namespace

  ----            ----      ---------

  ServiceAccount  kube-dns  kube-system



기본 계정(ServiceAccount)에 클러스터 관리자 권한을 부여한다면?


Kubernetes 는 클러스터 전체를 관리할 수 있는 권한을 'cluster-admin' 이라는 'ClusterRole' 형태로 제공하고 있다. 다음의 yaml 파일을 적용해 보자.


#  vi 00-default-admin-access.yaml 

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: cluster-admin-clusterrolebinding-1
subjects:
- kind: ServiceAccount
  name: default
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: cluster-admin-clusterrolebinding-2
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin

# kubectl apply -f 00-default-admin-access.yaml 

clusterrolebinding "cluster-admin-clusterrolebinding-1" created

clusterrolebinding "cluster-admin-clusterrolebinding-2" created

* 모든 네임스페이스에는 default 라는 계정이 반드시 하나 존재하게 되는데, 이들 계정에 cluster-admin 에 부여된 클러스터 관리자 권한을 매핑(ClusterRoleBinding)하여 적용한다



위와 같이 해 놓고, 앞서 확인용으로 만들어 놓은 check-apiserver-access-by-default.sh 를 실행하여 API 실행 권한을 확인해 보자.


# ./check-apiserver-access-by-default.sh 

HTTP/1.1 200 OK

Content-Type: application/json

Date: Tue, 05 Dec 2017 07:35:09 GMT

Transfer-Encoding: chunked


{

  "kind": "ServiceList",

  "apiVersion": "v1",

...

      "status": {

        "loadBalancer": {

          

        }

      }

    }

  ]

}



Default:default 계정으로 이전에는 동작하지 않던 default 네임스페이스의 서비스 목록 조회가 이번에는 정상적으로 동작한다(kube-system:default 계정으로도 마찬가지 결과를 확인할 수 있을 것이다).


그런데, 뭔가 석연치 않다. Kubernetes 의 기본 RBAC 설정을 망가뜨리고 있다는 생각이 들지 않는가? 바꿔 말해서 각 네임스페이스에 기본으로 존재하는 default 계정에 클러스터 전체를 관리할 수 있는 막강한 권한을 부여하는 것 자체가 보안성을 위협하는 심각한 장애 포인트가 될 수도 있다는 생각을 지울 수 없다. 일단 RBAC에 의한 권한 설정과 사용에 대한 감은 잡았으니, 깔끔하게 방금 설정한 내용을 지워 버리도록 하자..


# kubectl delete -f 00-default-admin-access.yaml 

clusterrolebinding "cluster-admin-clusterrolebinding-1" deleted

clusterrolebinding "cluster-admin-clusterrolebinding-2" deleted

* Yaml 파일 내용을 잘 보면, ClusterRoleBinding, 즉 권한 매핑을 지우는 것이지, default 계정들을 삭제하는 것은 아니므로 안심하자



클러스터 관리자 권한을 가지는 별도의 특별한 계정을 생성하고 사용


Kubernetes 클러스터 전체를 관리하는 유일하고 특별한 계정(kube-system:root-sa)를 따로 만들어서, 소수의 관리자 권한을 가진 사용자에게만 공개한다면 시스템 보안에 대한 위협은 훨씬 줄어들 것이다. 다음과 같이 yaml 파일을 작성하고 적용해 보자.


# vi 00-root-sa-admin-access.yaml 

kind: ServiceAccount
apiVersion: v1
metadata:
  name: root-sa
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: root-sa-kube-system-cluster-admin
subjects:
- kind: ServiceAccount
  name: root-sa
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin

# kubectl apply -f 00-root-sa-admin-access.yaml

clusterrolebinding "root-sa-kube-system-cluster-admin" created

* kube-system:root-sa 계정을 생성하고 cluster-admin 이라는 ClusterRole(권한 집합)과 매핑(ClusterRoleBinding)하여 클러스터 관리자 권한을 부여한다



참고로, 위의 yaml 로 정의된 root-sa 계정의 RBAC 설정은 다음 그림과 같이 이해하면 된다.




이제 앞서와 마찬가지로 방금 설정한 권한의 매핑 상태를 확인하는 bash script 를 하나 작성해 두자


# vi check-apiserver-access-by-root.sh

#!/bin/bash
# By modified RBAC policy, this 'curl' results in '200 OK'
APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")
# Retrieve 'root' account's TOKEN in 'kube-system' namespace
ROOTTOKEN="$(kubectl get secret -nkube-system $(kubectl get secrets -nkube-system | grep root-sa | cut -f1 -d ' ') -o jsonpath='{$.data.token}' | base64 --decode)"
curl -D - --insecure --header "Authorization: Bearer $ROOTTOKEN" $APISERVER/api/v1/namespaces/default/services

# chmod a+x check-apiserver-access-by-root.sh

* Kube-system:root-sa 계정의 토큰 키값을 사용해서 API 호출을 수행하는 bash script



현재 설정된 root-sa 의 권한을 확인하기 위해 위의 스크립트를 실행해 보자


# ./check-apiserver-access-by-root.sh 

HTTP/1.1 200 OK

Content-Type: application/json

Date: Tue, 05 Dec 2017 08:12:38 GMT

Transfer-Encoding: chunked


{

  "kind": "ServiceList",

  "apiVersion": "v1",

  "metadata": {

    "selfLink": "/api/v1/namespaces/default/services",

...

    }

  ]

}

* root-sa 계정으로 default 네임스페이스의 서비스 목록을 성공적으로 불러 오고 있음을 확인할 수 있다.



특정 대상(subject)에 적용된 권한 매핑(ClusterRoleBinding)과 권한 집합(ClusterRole) 조회


[Bonus]


Kubectl 명령으로 ClusterRole/ClusterRoleBinding 목록을 확인하려면,


# kubectl get clusterrole

NAME                                                                   AGE

admin                                                                  19d

cluster-admin                                                          19d

edit                                                                   19d

...

view                                                                   19d

weave-net                                                              19d


# kubectl get clusterrolebinding

NAME                                            AGE

cluster-admin                                   19d

heapster                                        19d

...

system:node                                     19d

system:node-proxier                             19d

weave-net                                       19d


와 같이 명령을 실행하면 되지만, 역으로 특정 대상(ServiceAccount/User/Group)과 연관된 ClusterRole이나 ClusterRoleBinding의 목록을  확인하고 싶을 경우 다음의 bash script 를 작성해서 사용한다면 요긴하게 쓰일 것이니 참고하여 보도록 하자. 단, 스크립트 내에서 JSON 입력을 파싱하는 기능을 사용하기 위해 jq(JSON processor)를 사용하므로 yum install jq 와 같이 jq 패키지를 설치해 두어야 한다.


# vi check-clusterrole.sh

#!/bin/bash
# $1 is kind (User, Group, ServiceAccount)
# $2 is name ("system:nodes", etc)
# $3 is namespace (optional, only applies to kind=ServiceAccount)
function getRoles() {
    local kind="${1}"
    local name="${2}"
    local namespace="${3:-}"

    kubectl get clusterrolebinding -o json | jq -r "
      .items[]
      | 
      select(
        .subjects[]?
        | 
        select(
            .kind == \"${kind}\" 
            and
            .name == \"${name}\"
            and
            (if .namespace then .namespace else \"\" end) == \"${namespace}\"
        )
      )
      |
      (.metadata.name + \" \" + .roleRef.kind + \"/\" + .roleRef.name)
    "
}

if [ $# -lt 2 ]; then
    echo "Usage: $0 kind name {namespace}"
    exit -1
fi

echo "* Query: "
echo " - kind: $1"
echo " - name: $2"
echo " - namespace: $3"
echo "* Result: "
echo "[ClusterRolebinding] [Clusterrole.kind/Clusterrole.name]"
getRoles $1 $2 $3

# chmod a+x check-clusterrole.sh

* Kubernetes 에서 권한(Role)을 매핑 또는 부여(Binding)하는 대상(subject)는 ServiceAccount/User/Group 중 하나이다 

* ServiceAccount 는 네임스페이스에 종속적이므로 kind=ServiceAccount 일 때는 네임스페이스를 지정해야만 한다



사용법은 다음과 같다. 각각의 실행 결과를 직접 확인해 보면 kubernetes 에서 제공하는 RBAC 메커니즘에 에 대한 이해에 도움이 될 듯하다.


# ./check-clusterrole.sh 

Usage: ./check-clusterrole.sh kind name {namespace}


# ./check-clusterrole.sh ServiceAccount root-sa kube-system

* Query: 

 - kind: ServiceAccount

 - name: root-sa

 - namespace: kube-system

* Result: 

[ClusterRolebinding] [Clusterrole.kind/Clusterrole.name]

root-sa-kube-system-cluster-admin ClusterRole/cluster-admin


# ./check-clusterrole.sh Group system:unauthenticated

* Query: 

 - kind: Group

 - name: system:unauthenticated

 - namespace: 

* Result: 

[ClusterRolebinding] [Clusterrole.kind/Clusterrole.name]

system:basic-user ClusterRole/system:basic-user

system:discovery ClusterRole/system:discovery


# ./check-clusterrole.sh User system:kube-scheduler

* Query: 

 - kind: User

 - name: system:kube-scheduler

 - namespace: 

* Result: 

[ClusterRolebinding] [Clusterrole.kind/Clusterrole.name]

system:kube-scheduler ClusterRole/system:kube-scheduler



- Barracuda -



  1. kube-dns는 k8s 의 클러스터 내부에 존재하는 DNS로, 각 서비스나 엔드포인트들의 IP 주소를 내부 도메인명인 *.cluster.local 과 연결시켜 resolution을 수행한다 [본문으로]