[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 계정으로 할 수 있는 것일까? 답은 "아니오" 이다. 맨 위의 그림이 그에 대한 설명을 보여 주고 있다. 다시 말하면 kube-system:kube-dns 계정(subject; 대상)에게는 전체 클러스터의 모든 서비스와 엔드포인트(cluster-wide, 즉 네임스페이스에 상관 없이) 들에 대해 'list', 'watch' 만 가능하도록 사전 설정 되어 있기 때문이다. 1
아래의 수행 결과는 현재 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 -
- kube-dns는 k8s 의 클러스터 내부에 존재하는 DNS로, 각 서비스나 엔드포인트들의 IP 주소를 내부 도메인명인 *.cluster.local 과 연결시켜 resolution을 수행한다 [본문으로]