[Kubernetes in Action] Securing the Kubernetes API server

2022. 1. 26. 22:52DevOps/Docker & Kubernetes

 

 

 

Understanding Authentication

이전 포스팅에서 살펴본 것처럼, 쿠버네티스 API 서버는 1개 이상의 Authentication(인증) 플러그인을 사용해서 구성될 수 있고, API 서버가 요청을 받았을 때 이 플러그인의 리스트를 통과하면서 인증을 거쳐 Authorization(권한 부여) 단계로 넘어가게 됩니다. 이 플러그인의 리스트를 거치면서 username, user ID, 클라이언트가 속하는 그룹(groups)에 대한 정보를 서버에 전달해주게 됩니다.

 

 

 

Users and Groups

 

Understanding Users

쿠버네티스에서는 API 서버에 연결하는 주체를 크게 2종류로 분류합니다.

  • Actual Humans (users)
  • Pods (Application running inside them)

실제 사람의 경우 제 3자(SSO 등)에 의해 관리되므로 쿠버네티스의 관심 대상이 아닙니다. 따라서 실제 사람의 계정을 관리하는 쿠버네티스의 리소스는 존재하지 않습니다. (따라서 사용자가 직접 API 서버에 요청해서 리소스에 CRUD 요청을 보낼 수 없습니다.) 반면, Pod는 쿠버네티스가 관리하는 대상이므로 쿠버네티스의 관심 대상이며, Service Accounts라는 메커니즘을 사용해서 인증됩니다. Service Accounts는 클러스터 내에서 저장되고 생성되는 리소스입니다.

 

Understanding Groups

모든 사용자(Actual Humans + Service Accounts)는 하나 이상의 Group에 속할 수 있으며 이 Group을 사용하게 되면 여러 명의 user에게 동시에 권한을 줄 수 있습니다. (AWS의 IAM을 생각하면 이해하기가 조금 쉬운 것 같습니다.) 위에서 API 서버의 인증 플러그인을 거치게 되면 username, userID외에 group에 대한 정보를 준다고 이야기했었는데, 실제로 이 정보는 다음과 같은 형식을 가진 단순한 스트링입니다. (아래는 몇 가지 내장된 group string입니다.)

 

  • system:unauthenticated
    • API 서버 내의 어떤 인증 플러그인도 인증할 수 없는 경우에 리턴합니다.
  • system:authenticated
    • 인증이 성공한 사용자에게 자동으로 설정됩니다
  • system:serviceaccounts
    • 시스템에 존재하는 모든 service account를 포함합니다.
  • system:serviceaccounts:<namespace>
    • 특정 namespace 안의 모든 service account를 포함합니다.

 

 

Introducing ServiceAccounts

Pod Metadata를 다룬 이전 포스팅에서 클라이언트가 쿠버네티스 API 서버에게 어떤 요청을 보내 수행하게 하기 위해서는 인증 과정이 필요하며, 파드가 이 인증 과정을 통과하기 위해 파드의 컨테이너의 /var/run/secrets/kubernetes.io/serviceaccount/token 에 들어있는 토큰 정보를 사용한다는 것을 배웠습니다. 

 

 

실제로 Secret과 ConfigMap을 다룬 이전 포스팅에서 살펴보았듯, 기본적으로 쿠버네티스가 가지고 있는 secret은 파드가 생성될 때 마운트 되며, 이 secret은 container의 fileSystem에 마운트 되어 컨테이너 내에서 접근 가능하게 됩니다. 위에서 볼 수 있듯, ca.crt, namespace, token이 들어있게 되며 여기 있는 토큰을 사용해서 API 서버와의 통신이 가능하게 됩니다. 실제로 API 서버에 요청을 보낼 때 API 서버의 인증 플러그인(authentication)은 이 토큰을 사용해서 해당 Service Account를 인증하고 API 서버에게 ServiceAccount username을 알려줍니다. 이후에 권한 부여 플러그인(authorization)으로 넘겨서 실제로 해당 유저가 해당 요청을 보낼 자격이 있는지를 검증하게 됩니다.

 

결국 ServiceAccount는 파드내의 애플리케이션이 API 서버와 통신할 때 자신을 증명(authentication)하는 하나의 방법인 것이며 다음과 같은 형태의 username을 갖습니다.

 

system:serviceaccount:<namespace>:<service account name>

 

 

 

 

Understanding the ServiceAccount Resource

ServiceAccount도 Pods, Secrets와 같은 쿠버네티스 리소스이며, 그렇기 때문에 특정 namespace에 귀속됩니다. default ServiceAccount는 개별 namespace에 기본적으로 설정되어있고, 이를 default로 사용합니다.

 

 

각 파드는 오직 하나의 ServiceAccount와 연결될 수 있지만, 같은 ServiceAccount를 여러 개의 파드에서 재사용할 수 있습니다. 단 ServiceAccount는 namespace단위의 리소스이므로 다른 namespace에 있는 ServiceAccount를 사용해서 인증을 할 수는 없습니다.

 

 

Understanding how ServiceAccounts Tie into Authorization

기본적으로 별도로 파드 manifest 파일에 ServiceAccount를 지정하지 않으면 파드가 속한 namespace에 자동으로 생성되는 ServiceAccount를 사용합니다. 별도로 ServiceAccount를 지정하면 해당 ServiceAccount를 사용하게 되며 이렇게 함으로써 특정 파드가 접근할 수 있는 리소스에 대한 권한을 정해줄 수 있게 됩니다. 

 

API 서버가 ServiceAccount의 토큰을 사용해서 인증한 뒤에는 role-base access control(RBAC)와 같은 권한 부여(authorization) 플러그인을 사용해서 해당 토큰으로 요청한 리소스에 접근하여 요청을 수행할 권한이 있는지를 확인하게 됩니다.

 

 

Creating ServiceAccounts

namespace에 기본적으로 생성되는 ServiceAccount가 있지만, 권한부여는 가급적 필요한 만큼만 최소로 하는 것이 좋습니다. 따라서 파드의 용도에 맞게 ServiceAccount를 직접 설정하고 파드에 연결해야 합니다. ServiceAccount는 쿠버네티스 리소스이기 때문에 다음과 같은 명령어로 간단하게 생성할 수 있습니다.

 

kubectl create serviceaccount foo

 

실제로 이렇게 생성된 ServiceAccount를 살펴보면 다음과 같습니다. 

 

  • tokens
    • ServiceAccount 생성시에 custom token Secret이 생성되게 되며 kubectl describe secret <token-name> 명령어를 사용해서 해당 시크릿의 정보를 확인할 수 있습니다. 위에서 확인했던 것처럼 ca.crt, namespace, token 정보다 포함되어 있음을 알 수 있습니다.
  • mountable secrets
    • 파드는 manifest를 통해 원하는 secret를 임의로 마운트 할 수 있지만 ServiceAccount에서 이  ServiceAccount를 사용하는 파드가 마운트 할 수 있는 Secret를 제한할 수 있습니다. mountable secrets는 이 제한된 Secret의 목록을 의미하며 이 목록에 없는 secret은 사용할 수 없습니다.
  • image pull secrets
    • private image repository에서 image를 pull 받을때 사용하는 secret입니다. 위의 mountable secrets처럼 사용 가능한 secret을 제한하는 방식이 아니며 위의 리스트에 있는 secret들은 해당 ServiceAccount를 사용하는 파드에 자동으로 mount 됩니다.

 

 

 

Assigning a ServiceAccount to a Pod

다음과 같이 파드 manifest에 ServiceAccount를 명시하여 사용할 수 있습니다. 파드가 생성될 때 미리 정해야 하며, 파드가 생성된 뒤에는 수정할 수 없습니다.

apiVersion: v1
kind: Pod
metadata:
  name: yeoul-custom-sa
spec:
  serviceAccountName: foo
  containers:
  - name: yeoul
  ...

 

 

Securing the cluster with role-based access control

위에서 ServiceAccount를 통해 쿠버네티스 API 서버에서 인증(Authentication)을 처리하는 방식을 살펴보았습니다. 인증이 마무리된 이후에는 적절한 권한 부여(Authorization)를 통해 인증된 사용자가 해당 요청을 수행할 권한이 있는지를 확인해야 합니다. 위에서 언급했듯, 쿠버네티스 API 서버에서는 Authorization Plugin을 통해 권한 부여를 수행할 수 있는 기능을 제공하며 이번 포스팅에서는 RBAC(Role-Based Access Control) 플러그인에 대해서 알아보도록 하겠습니다.

 

 

Introducing the RBAC authorization Plugin

API 서버는 REST Interface를 노출하기 때문에 사용자는 HTTP 요청을 서버로 보내게 됩니다. ServiceAccount를 통해 사용자를 인증하며, 인증된 사용자가 해당 요청을 수행할 권한이 있는지를 Authorization Plugin을 통해 판단하게 됩니다.

 

Understanding Actions

REST 클라이언트는 GET, POST, PUT, DELETE와 같은 HTTP 요청들을 특정한 URL(원하는 리소스가 있는 주소)로 보내게 됩니다. 쿠버네티스에서 각각의 URL에 위치한 리소스들은 Pods, Services, Secrets와 같은 쿠버네티스 리소스를 의미합니다. "파드를 가져온다", "시크릿을 업데이트한다"와 같은 요청들은 각각 HTTP 요청들로 매핑되게 되며, RBAC와 같이 쿠버네티스 API 서버 안에서 동작하는 authorization plugin에서 해당 요청들의 권한 여부를 체크하게 됩니다.

 

Understanding the RBAC Plugin

RBAC 플러그인은 user의 role을 기반으로 권한 부여를 하게 됩니다. 여기서의 user는 사용자(real human)와 ServiceAccount를 모두 포함하는 개념이며, user에게는 하나 이상의 role이 부여됩니다. 각 role에는 어떤 리소스에 어떤 작업을 할 수 있는지가 정해져 있습니다. 만약 하나의 유저에게 여러개의 role이 부여된다면 role에서 부여한 권한들의 합이 해당 user의 권한이 됩니다.

 

 

Introducing RBAC resources

RBAC의 권한부여 규칙은 4개의 리소스를 통해 설정됩니다. 각각 Roles, ClusterRoles, RoleBindings, ClusterRoleBindings로 구분되며, "무엇(What)"을 할 수 있는지와 "누가(Who)"할 수 있는지에 따라 다음의 2가지 분류로 나뉘게 됩니다.

 

  • Roles, ClusterRoles: "What"에 해당하며 어떤 리소스에 어떤 작업을 할 수 있는지를 관리합니다.
  • RoleBindings, ClusterRoleBindings: "Who"에 해당하며, 위 Role을 적용할 user, group, serviceAccounts를 관리합니다.

 

앞에 "Cluster"가 붙은 리소스는 namespace가 아닌 Cluster 레벨의 리소스를 관리한다는 의미이며 그렇지 않은 경우는 특정 namespace의 리소스를 관리한다는 의미입니다.

 

 

위의 내용을 도식화 하면 아래와 같습니다.

 

 

Using Roles and RoleBindings

Creating Roles

Role은 다음과 같이 생성할 수 있습니다. 아래는 Service List를 조회할 수 있도록 권한을 주는 Role입니다. 리소스를 명시할 때는 반드시 복수형으로 적어야 하며, 리소스의 특정 인스턴스에만 접근을 허용하고 싶은 경우에는 resourceNames 필드를 추가해주면 됩니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: foo
  name: service-reader
rules:
- apiGroups: [""]
  verbs: ["get", "list"]
  resources: ["services"]

 

실제로 Role은 리소스이므로 다른 쿠버네티스 리소스와 같이 kubectl를 사용해서 생성할 수 있습니다. 단, namepsace에 주의해야 합니다.

kubectl create -f service-reader.yaml -n foo

 

 

Binding a role to a ServiceAccount

이제 이 Role을 ServiceAccount에 적용해야 합니다. 이를 위해서 RoleBinding 리소스를 생성해야 합니다.

kubectl create rolebinding test --role=service-reader \
--serviceaccount=foo:default -n foo

 

user에게 bind하려면bind 하려면 --user, group에게 bind 하려면 --group 옵션을 주면 됩니다. Role 자체는 위에서 언급했듯 재사용이 가능하기 때문에 여러 RoleBinding을 생성할 수 있습니다.

 

 

Including ServiceAccounts from othe namespaces in a RoleBinding

RoleBinding을 수정해서 다른 namespace에 있는 서비스 목록을 조회할 수 있도록 권한을 추가해보겠습니다.

kubectl edit rolebinding test -n foo
subjects:
- kind: ServiceAccount
  name: default
  namespace: foo
- kind: ServiceAccount
  name: default
  namespace: bar

 

이렇게 해서 foo namespace에 있는 service를 읽을 수 있는 roleBinding에 foo namespace의 ServiceAccount와 bar namespace의 ServiceAccount를 묶어두었습니다. 따라서 bar namespace안에 있는 pod에서도 foo namespace의 Service를 읽을 수 있게 됩니다. 

 

 

 

Using ClusterRoles and ClusterRoleBindings

위에서 살펴본 Roles와 Rolebindings는 특정 namespace안에 있는 리소스에 대한 권한만 부여할 수 있습니다. 따라서 여러 namespace에 접근해야 하는 경우, 각 namespace마다 Role과 RoleBinding을 각각 만들어주어야 하는 불편함이 있습니다. 뿐만 아니라 이전 챕터들을 살펴보면서 쿠버네티스 리소스 중에 namespace에 속하지 않는 Node, Persistent Volume, Namespace와 같은 리소스들이 있고 API 서버의 endpoint 중에 리소스를 나타내지 않고 있는 URLs(e.g /health) 도 있기 때문에 Role만 사용해서는 위와 같은 리소스들에 대한 접근 권한을 관리할 수 없게 됩니다.  위와 같은 문제를 해결하기 위해 Cluster단위로 관리할 수 있는 ClusterRole과 ClusterRoleBinding이 존재하게 됩니다.

 

Allowing access to cluster-level resources

우선 Namespace가 없음에 유의하며 다음과 같이 ClusterRole을 생성해 봅니다.

kubectl create clusterrole pv-reader --verb=get, list \
--resource=persistentvolumes

생성한 이후에 ClusterRoleBinding을 생성해 봅니다.

kubectl create clusterrolebinding pv-test --clusterrole=pv-reader \
--serviceaccount=foo:default

위와 같이 연결하게 되면 foo namespace안의 ServiceAccount를 사용하는 Pod들이 클러스터 레벨의 리소스인 PersistentVolumes에 접근할 수 있게 됩니다.

 

 

Allowing access to non-resource URLs

실제로 리소스를 Reference하지 않는 URL에 대한 요청 권한도 직접 허용해주어야 합니다. 대부분의 경우 default로 system:discovery라는 ClusterRole / ClusterRoleBinding이 존재하며, 이를 통해 해결할 수 있습니다. 실제로 system:discovery ClusterRole이 어떻게 구성되어 있는지 확인해보겠습니다.

 

kubectl get clusterrole system:discovery -o yaml

 

 

rules에 nonResourceURLs로 명시되어있고 기본적으로 'get' 요청을 허용하는 ClusterRole이 존재하는 것을 확인할 수 있습니다. 실제로 ClusterRole은 반드시 ClusterRoleBindings와 연결되어야 하며 nonResourceURL에 대한 ClusterRole이 RoleBindings와 연결되는 것은 아무런 효과가 없습니다. (실제로 ClusterRole이 RoleBindings와 연결될 수는 있지만, 이 경우는 nonResourceURL에 대한 Role은 아닙니다.)

 

 

실제로 system:discovery ClusterRole은 system:discovery ClusterRoleBinding과 연결되며 이를 조회해보면 다음과 같습니다.

kubectl get clusterrolebinding system:discovery -o yaml

 

 

Using ClusterRoles to grant access to resources in specific namespaces

ClusterRole이 항상 ClusterRoleBindings와 연결되어야 하는 것 은 아닙니다. ClusterRole은 Rolebindings에 연결될 수도 있고, ClusterRoleBindings에 연결될 수도 있습니다. RoleBindings에 연결되는 경우 RoleBindings는 namespace가 있으므로 해당 namespace안에 있는 리소스에 대해 권한이 부여됩니다. Role을 여러 namespace에서 재사용하고 싶을 때 ClusterRole 을 생성한 뒤에 RoleBindings로 묶어서 재사용하는 방법을 사용하기도 합니다.

 

 ClusterRoleBindings에 연결되는 경우 임의의 namespace안에 있는 리소스에 대해 권한이 부여됩니다.

 

 

반응형