[AWS EKS Workshop Study] 6주차 - 보안(인증/인가)

JoonHyeok Han·2024년 4월 13일
0

개요

이번 6주차 EKS 스터디에서는 보안에 대해서 학습했다.

지금까지 다루었던 주제들 중에서 가장 양이 많은 것 같고, 개념들도 복잡해서 굉장히 어려웠다.

보안이 중요한 이유는 누군가 취약점을 이용해서 AWS 계정을 마음대로 이용하는 아찔한 일이 발생할 수도 있기 때문이다.

또한, EKS 를 사용하는 내부 개발자들에게 모든 권한을 갖게 되면 시스템 전체에 영향을 줄 수 있기 때문에 이를 방지하기 위해 제한된 최소 권한만 부여해야 한다.

실습과 함께 생소한 개념들을 위주로 정리했다.

서비스 어카운트(Service Account)

출처: Service Account | Kubernetes [medium]

서비스 어카운트는 쿠버네티스에서만 사용되는 개념으로, 파드에서 실행되는 프로세스를 위해 할당된다.

서비스 어카운트는 쿠버네티스 클러스터 내의 다른 자원(볼륨, 서비스, 파드 등)에 접근할 수 있는지 판단하기 위해 사용된다.

즉, API 서버와 통신해서 클러스터 내의 다른 자원에 대한 인증 및 인가를 하기 위해 사용된다.

이를 이해하기 위해서는 Role 과 RoleBinding 을 이해해야 하는데, 이는 뒤에서 다시 살펴보자.

또한, 서비스 어카운트는 네임스페이스와 연결된 리소스다.

파드를 실행할 때 반드시 서비스 어카운트 한 개를 할당해야 한다.

별도로 지정하지 않으면 기본 서비스 어카운트가 할당된다.

유저 어카운트(User Account)

유저 어카운트는 구글 계정과 같은 사용자의 ID 를 의미한다.

쿠버네티스에는 일반 사용자를 나타내는 오브젝트가 존재하지 않는 대신 일반 사용자의 관리를 AWS, Google 계정과 같은 외부 시스템이 맡기고 있다.

일반 사용자는 외부 시스템을 통해 인증을 거쳐야 쿠버네티스의 리소스를 조작할 수 있다.

인증 및 인가에 유저 어카운트(User Account)를 사용하지 않는 이유

쿠버네티스에서 유저 어카운트를 기반으로 클러스터의 자원에 대한 인증 및 인가를 사용하지 않는 이유는 관리의 복잡도를 낮추기 위해서다.

모든 사용자가 클러스터 내의 자원을 모두 접근하도록 하는 것은 시스템에 영향을 크게 미칠 수 있기 때문에 각 사용자는 적절한 권한을 가지도록 설정해줘야 한다.

하지만 유저 어카운트를 사용하면 새로운 사용자가 추가되거나 기존 사용자의 역할이 변경될 때마다 적절한 권한을 할당하고 업데이트 해주어야 하는 번거로움이 존재한다.

위의 이미지처럼 각각의 사용자가 각 네임스페이스의 정해진 파드에 대해서만 권한을 주도록 하는 것은 사용자마다 직접 일일이 권한을 분배해주어야 하기 때문에 비효율적이다.

그래서 쿠버네티스는 사용자가 아닌 파드 중심으로 서비스 어카운트와 Role, RoleBinding 이라는 개념을 이용해서 관리의 복잡도를 낮추었다.

서비스 어카운트 예시

서비스 어카운트 생성

default 네임스페이스에 demo-sa 라는 이름을 가진 서비스 어카운트를 생성하는 yaml 파일은 아래와 같다.

# demo-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: demo-sa
  namespace: default

아래의 명령어를 실행하면 default 네임스페이스에 서비스 어카운트를 생성할 수 있다.

kubectl apply -f demo-sa.yaml

서비스 어카운트 정보를 확인하려면 아래의 명령어를 실행한다.

kubectl get sa demo-sa -o yaml

실행 결과는 아래와 같다.

apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2024-04-08T14:08:44Z"
  name: demo-sa
  namespace: default
  resourceVersion: "141929"
  uid: ac443139-3976-4936-b536-f99d54e46f47

파드에 서비스 어카운트 할당하기

파드에 서비스 어카운트를 지정해서 생성한다면 아래와 같이 yaml 파일을 작성할 수 있다.

# demo-sa-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: demo-sa-pod
  namespace: default
spec:
  serviceAccountName: demo-sa
  containers:
  - name: demo-nginx
    image: nginx:1.16

spec.serviceAccountName 속성을 이용하면 파드에 서비스 어카운트를 할당할 수 있다.

아래의 명령어를 실행해서 파드를 실행한다.

kubectl apply -f demo-sa-pod.yaml

자세한 정보를 확인하기 위해 아래의 명령어를 실행한다.

kubectl get pod demo-sa-pod -o yaml

실행 결과는 아래와 같다.

apiVersion: v1
kind: Pod
metadata:
  ...
  name: demo-sa-pod
  namespace: default
  ...
spec:
  ...
  serviceAccount: demo-sa # 파드에 할당된 서비스 어카운트
  serviceAccountName: demo-sa
  ...

서비스 어카운트가 파드에 할당된 것을 spec.serviceAccount 속성에서 확인할 수 있다.

Role 과 RoleBinding

Role 은 어떤 조작을 허용할 지 결정하는 것이다.

RoleBinding 은 서비스 어카운트에 Role 을 연결해서 권한을 부여하는 것이다.

Role 과 RoleBinding 은 네임스페이스 수준의 리소스와 클러스터 수준의 리소스 총 2가지가 존재한다.

네임스페이스 수준에서는 Role, RoleBinding 이라고 하며, 클러스터 수준에서는 ClusterRole, ClusterRoleBinding 이라고 한다.

Role, RoleBinding 생성

위에서 생성한 서비스 어카운트가 사용할 수 있는 권한을 정의하는 Role 을 작성하기 위해 아래와 같은 yaml 파일을 작성했다.

# demo-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: demo-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "delete"]

demo-role 이란 이름을 가진 Role 은 default 네임스페이스에서 파드에 대해 get, delete 를 수행할 수 있도록 했다.

demo-role 과 demo-sa 를 연결하기 위한 RoleBinding 은 아래의 yaml 파일을 사용했다.

# demo-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: demo-role-binding
  namespace: default
subjects:
- kind: ServiceAccount
  name: demo-sa
  namespace: default
roleRef:
  kind: Role
  name: demo-role
  apiGroup: rbac.authorization.k8s.io

demo-role-binding 이란 이름을 가진 RoleBinding 객체는 RoleBinding 이 될 대상으로 subjects 에 작성했다. 여기서는 default 네임스페이스의 서비스 어카운트 demo-sa 으로 지정했다.

그리고 RoleBinding 에 바인딩 할 Role 을 roleRef 에 작성했다. Role 의 이름은 demo-role 로 지정했다.

아래의 명령어를 실행해서 Role 과 RoleBinding 객체를 생성한다.

kubectl apply -f demo-role.yaml
kubectl apply -f demo-role-binding.yaml

RBAC 적용 확인

서비스 어카운트와 Role 이 RoleBinding 을 통해 연결되었으니 적용 되었는지 확인하려면 파드 안에서 kubectl 을 실행해보면 된다.

파드에서 클러스터 내의 default 네임스페이스에 존재하는 파드에 대한 정보를 받아올 수 있는지 확인해보자.

이를 위해 kubectl 가 설치된 컨테이너를 실행했고, 아래의 yaml 파일을 사용했다.

# demo-kubectl.yaml

apiVersion: v1
kind: Pod
metadata:
  name: demo-kubectl
spec:
  serviceAccountName: demo-sa
  containers:
  - name: kubectl-container
    image: bitnami/kubectl:latest
    command: ["tail", "-f"]

아래의 명령어를 실행해서 파드를 실행한다.

kubectl apply -f demo-kubectl.yaml

간편하게 컨테이너 내부에서 명령어를 실행할 수 있도록 아래의 alias 를 설정했다.

alias k1='kubectl exec -it demo-kubectl -- kubectl'

그 다음 아래의 명령어를 실행해서 pod 정보를 조회했다.

k1 get pod

실행 결과는 아래와 같다.

NAME           READY   STATUS    RESTARTS   AGE
demo-kubectl   1/1     Running   0          88s
demo-sa-pod    1/1     Running   0          31m

이제 Role 에서 파드의 정보를 받아올 수 있는 get 권한을 제거하기 위해 demo-role.yaml 파일에서 verbs 를 삭제했다.

# demo-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: demo-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list"] # get 삭제

아래의 명령어로 다시 Role 을 적용했다.

kubectl apply -f demo-role.yaml

동일하게 파드의 정보를 받아오는 get 명령어를 실행했다.

k1 get pod

하지만 아래와 같이 권한이 없다고 표시되는 것을 확인할 수 있다.

Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:demo-sa" cannot list resource "pods" in API group "" in the namespace "default"
command terminated with exit code 1

쿠버네티스의 인증과 인가

쿠버네티스 클러스터에 리소스(파드 등)를 등록하기 위해서는 쿠버네티스 API 서버에 요청을 보내야 한다.

출처: Chapter 17. Admission Control and Authorization [oreilly]

쿠버네티스 API 서버는 API 요청을 받으면 아무나 클러스터에 리소스를 등록하면 안되기 때문에 리소스 등록이 허용된 사용자인지 확인하는 인증(Authentication)을 하고, 사용자의 요청이 수행하고자 하는 작업이 허용되는지 판단하는 인가(Authorization)을 수행한다.

인증과 인가 과정이 끝나면 어드미션 컨트롤(Admission Control)에 의해 요청된 작업이 허용되는지 검증하고, 리소스를 변경하는 작업을 수행한다.

인증(Authentication)

kubectl get pod 와 같은 명령어를 실행하면 쿠버네티스 config 파일(~/.kube/config) 에서 현재 사용자가 누구인지 확인한다.

config 파일에는 아래와 같은 정보가 저장되어 있다.

  1. 클러스터 정보: 클러스터 이름, 서버 URL, CA 인증서 경로
  2. 사용자 정보: 각 사용자에 대한 자격 증명 정보(이메일, 토큰, 인증서, 개인키 등)
  3. 컨텍스트(context): 클러스터 정보와 사용자 정보를 결합해서 실제로 사용할 클러스터 및 사용자를 지정

마스터 노드에서 확인한 해당 파일의 내용은 아래와 같다.

apiVersion: v1
clusters: # 클러스터 정보
- cluster:
    certificate-authority-data: ...
    server: https://10.0.2.15:6443
  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: ...
    client-key-data: ...

사용자 정보가 API 서버에 도착하면 쿠버네티스는 사용자를 인증하기 위해 아래와 같이 6가지 방법을 사용할 수 있다.

  1. 기본 인증(일반 유저): ID, 암호 또는 인증 토큰 이용
  2. 기본 인증(서비스 어카운트): JWT 이용
  3. X.509 인증서 기반 인증: X.509 인증서의 프라이빗키와 퍼블릭키 비교 방식 이용
  4. OAuth 연동 인증: OAuth 서비스 프로바이더와 연동하여 인증
  5. WebHook 연동 인증: WebHook 으로 인증 시스템과 연동하여 인증
  6. Proxy Server 연동 인증: Proxy Server 로 인증 시스템과 연동하고, X.590 인증서 기반 인증을 조합하여 인증

어떤 인증 방식이 더 낫다는 기준은 없지만, 서비스의 성격에 따라 편의성과 보안을 고려해서 적절한 인증 방식을 선택하면 된다.

인가(Authorization)

쿠버네티스에서 인가는 RBAC 을 기반으로 이루어진다.

사용자와 역할을 별도로 선언하고, 이 두 가지를 조합해서 사용자에게 권한을 적절하게 부여하는 방식이다.

앞서 설명한 서비스 어카운트 사용 예시는 인증을 거치고 나서 인가를 해주기 위해 서비스 어카운트, 롤, 롤바인딩을 이용한다는 것을 보여준 것이다.

EKS의 인증과 인가

EKS 에서 인증은 AWS IAM 을 통해 이루어지고, 인가는 쿠버네티스의 RBAC 을 통해 이루어진다.

조금 더 자세한 과정을 정리하면 아래의 이미지와 같다.

출처: EKS 환경을 더 효율적으로, 더 안전하게 [aws]

인증(Authentication)

EKS 토큰 발급

EKS 사용자가 kubectl 명령어를 실행하면 참고하는 ~/.kube/config 파일에는 AWS CLI 명령어를 수행하도록 설정되어 있다.

users:
- name: ...@myeks.ap-northeast-2.eksctl.io
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - eks
      - get-token
      - --output
      - json
      - --cluster-name
      - myeks
      - --region
      - ap-northeast-2
      command: aws
      env:
      - name: AWS_STS_REGIONAL_ENDPOINTS
        value: regional
      interactiveMode: IfAvailable
      provideClusterInfo: false

이 명령어를 보기 쉽게 바꾸면 아래와 같다.

aws eks get-token --cluster-name $CLUSTER_NAME \
--region ap-northeast-2

즉, kubectl 명령어를 실행하면 aws eks get-token 명령어가 실행되면서 STS 에 토큰을 요청한다.

  • STS(Security Token Service)는 AWS 리소스에 대한 접근을 제어할 수 있는 임시 보안 자격 증명을 생성하고, 이를 신뢰하는 사용자에게 제공한다.
  • 즉, 현재 kubectl 명령어를 실행하는 사용자가 EKS 클러스터에 접근할 수 있는 권한을 갖고 있다는 사용자라는 것을 STS 를 통해 임시로 증명할 수 있도록 토큰을 받는 것이다. 임시로 증명하는 이유는 증명의 유효 기간이 길면 누군가가 이를 탈취 했을 때 피해를 최소화 하기 위함이다.

pre-signed url 토큰 전달

aws eks get-token 명령어의 실행 결과로 토큰의 유효기간과 토큰을 받게 된다.

{
  "kind": "ExecCredential",
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "spec": {},
  "status": {
    "expirationTimestamp": "2024-04-13T11:55:39Z",
    "token": "k8s-aws-v1..."
  }
}

토큰을 decode 하면 pre-signed url 이 나온다.

https://sts.ap-northeast-2.amazonaws.com/?
Action=GetCallerIdentity&
Version=2011-06-15&
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=...ap-northeast-2%2Fsts%2Faws4_request&
X-Amz-Date=20240413T114139Z&
X-Amz-Expires=60&
X-Amz-SignedHeaders=host%3Bx-k8s-aws-id&
X-Amz-Signature=...

pre-signed url 은 임시로 AWS 리소스에 접근할 수 있는 임시 요청 링크이다.

kubectl 에 포함된 client-go 플러그인은 Bearer Token 으로 pre-signed url 이 담긴 토큰을 담아서 AWS STS 에 해당 토큰이 유효한지 확인한다.

aws-auth 파일 확인

EKS API 서버는 Webhook token authenticator 에 요청해서 STS 에서 받아온 Role 과 User ARN 값을 aws-auth configmap 에 매핑된 ARN 값과 비교한다.

동일한 값이라면 유저에게는 EKS API 서버에 접근할 수 있는 권한이 주어진다.

aws-auth configmap 을 확인하려면 아래의 명령어를 실행하면 된다.

kubectl get cm aws-auth -n kube-system -o yaml

실행 결과는 아래와 같다.

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::[ARN]:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-moV9GqIQ4O2J
      username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
  creationTimestamp: "2024-04-13T11:30:28Z"
  name: aws-auth
  namespace: kube-system
  resourceVersion: "1560"
  uid: ...

인가(Authorization)

AWS IAM 역할과 유저를 쿠버네티스 롤에 연동하는 것은 eksctl 명령어를 사용해서 할 수 있다.

eksctl create iamidentitymapping \
    --cluster $CLUSTER_NAME \
    --region=ap-northeast-2 \
    --arn <연동하고자하는 AWS ROLE / USER ARN 값> \
    --username <EKS 내에서 사용하는 Username> \
    --group <Role 사용 주체가 Group일 경우, 해당 Group Name> \
    --no-duplicate-arns

실습

EKS 관리자가 새로운 사람에게 IAM 계정을 생성해서 EKS 접근 권한을 부여하는 상황을 가정한 실습을 진행해보자.

IAM 계정 생성

EKS 관리자는 아래의 명령어를 실행해서 testuser 라는 이름의 IAM 계정을 생성했다.

aws iam create-user --user-name testuser

새로운 사용자가 이용할 수 있는 액세스 키와 비밀 키를 생성한다.

aws iam create-access-key --user-name testuser

실행 결과로 아래와 같이 출력된다.

{
    "AccessKey": {
        "UserName": "testuser",
        "AccessKeyId": "AKI...", # 아래에서 사용할 예정
        "Status": "Active",
        "SecretAccessKey": "uhOR...", # 아래에서 사용할 예정
        "CreateDate": "2024-04-13T12:03:44+00:00"
    }
}

testuser 사용자에게 AdministratorAccess 정책을 추가한다.

aws iam attach-user-policy \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
--user-name testuser

자격증명 설정 및 확인

새로운 사용자인 testuser 의 PC 에서 AWS 자격증명을 설정하기 위해 아래의 명령어를 실행한다.

aws configure
AWS Access Key ID [None]: AKI*...*
AWS Secret Access Key [None]: uhOR*...*
Default region name [None]: *ap-northeast-2*

액세스 키와 시크릿 키는 위에서 발급 받은 값을 입력한다.

아래의 명령어를 실행해서 자격증명을 확인할 수 있다.

aws sts get-caller-identity --query Arn
#"arn:aws:iam::...:user/testuser"

하지만 kubectl 명령어는 정상적으로 사용이 불가능하다.

kubectl get node -v6
#I0413 21:12:15.425118    3541 round_trippers.go:553] GET http://localhost:8080/api?timeout=32s  in 0 milliseconds
#E0413 21:12:15.425206    3541 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
...

왜냐하면 ~/.kube/config 파일이 존재하지 않기 때문인데, 이는 testuser 에게 EKS 관리자 권한이 부여되어야 생성할 수 있다.

즉, 인가에서 통과하지 못한 것이기 때문에 eksctl 로 IAM 계정과 쿠버네티스 롤을 연동해주어야 한다.

EKS 관리자 권한 설정

EKS 관리자 PC 에서 아래의 명령어를 실행해서 IAM 계정과 쿠버네티스 롤을 연동한다.

 eksctl create iamidentitymapping \
 --cluster $CLUSTER_NAME \
 --username testuser \
 --group system:masters \
 --arn arn:aws:iam::$ACCOUNT_ID:user/testuser

정상적으로 추가되었는지 확인하기 위해 아래의 명령어를 실행했다.

kubectl get cm -n kube-system aws-auth -o yaml

실행 결과는 아래와 같다.

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::...:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-moV9GqIQ4O2J
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::...:user/testuser
      username: testuser
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system

mapUsers.groups.userarn 에 testuser 가 추가되었고, 관리자 권한에 해당하는 system:masters 그룹에 속한 것을 확인할 수 있다.

kubeconfig 생성

testuser 의 PC 에서 kubeconfig 를 생성하기 위해 아래의 명령어를 실행한다.

aws eks update-kubeconfig \
--name $CLUSTER_NAME --user-alias testuser

아래의 명령어를 사용해서 생성된 config 파일을 확인할 수 있다.

cat ~/.kube/config

네임스페이스를 default 로 설정하고 kubectl 을 실행하면 정상적으로 실행되는 것을 확인할 수 있다.

kubectl ns default
kubectl get node -v6
#I0413 21:34:31.093589    7825 loader.go:395] Config loaded from file:  /root/.kube/config
#I0413 21:34:31.985049    7825 round_trippers.go:553] GET https://298AB16468D31515D0EEB2146BF016AB.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 200 OK in 880 milliseconds
#NAME                                               STATUS   ROLES    AGE   VERSION
#ip-192-168-1-186.ap-northeast-2.compute.internal   Ready    <none>   63m   v1.28.5-eks-5e0fdde
#ip-192-168-2-25.ap-northeast-2.compute.internal    Ready    <none>   63m   v1.28.5-eks-5e0fdde
#ip-192-168-3-77.ap-northeast-2.compute.internal    Ready    <none>   63m   v1.28.5-eks-5e0fdde

그룹 변경

EKS 관리자 PC 에서 testuser 의 그룹을 system:masters 에서 system:authenticated 로 변경했을 때 RBAC 이 작동하는 것을 확인해보자.

아래의 명령어를 실행해서 직접 수정할 수 있다.

kubectl edit cm -n kube-system aws-auth

그 다음 testuser 의 PC 에서 아래의 명령어를 실행해보자.

kubectl get node -v6

아래와 같이 노드 정보를 받아올 수 없다며 403 에러를 반환한다.

I0413 21:42:12.236295    1988 loader.go:395] Config loaded from file:  /root/.kube/config
I0413 21:42:12.999614    1988 round_trippers.go:553] GET https://298AB16468D31515D0EEB2146BF016AB.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 403 Forbidden in 755 milliseconds
I0413 21:42:12.999971    1988 helpers.go:246] server response object: [{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "nodes is forbidden: User \"testuser\" cannot list resource \"nodes\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
    "kind": "nodes"
  },
  "code": 403
}]
Error from server (Forbidden): nodes is forbidden: User "testuser" cannot list resource "nodes" in API group "" at the cluster scope

IAM 맵핑 삭제

testuser 가 EKS 를 더 이상 이용할 수 없도록 관리자 PC 에서 IAM 맵핑을 삭제해보자.

eksctl delete iamidentitymapping \
--cluster $CLUSTER_NAME \
--arn  arn:aws:iam::$ACCOUNT_ID:user/testuser

testuser 의 PC 에서 다시 kubectl 명령어를 실행하면 이번에는 401 Unauthorized 가 반환된다.

kubectl get node -v6
#I0413 21:55:33.154838    2414 loader.go:395] Config loaded from file:  /root/.kube/config
#I0413 21:55:34.879460    2414 round_trippers.go:553] GET https://298AB16468D31515D0EEB2146BF016AB.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 401 Unauthorized in 1713 milliseconds
#I0413 21:55:34.879738    2414 helpers.go:246] server response object: [{
#  "kind": "Status",
#  "apiVersion": "v1",
#  "metadata": {},
#  "status": "Failure",
#  "message": "Unauthorized",
#  "reason": "Unauthorized",
#  "code": 401
#}]
#error: You must be logged in to the server (Unauthorized)

IRSA

IRSA(IAM Role Service Account)는 AWS 에서 제공하는 기능이다.

쿠버네티스의 서비스 어카운트에 AWS IAM 역할을 할당해서 쿠버네티스 파드가 AWS 리소스에 안전하게 접근할 수 있도록 한다.

서비스 어카운트는 AWS 의 리소스가 아니지만 IAM 역할을 할당할 수 있는 이유는 OIDC(OpenID Connect) 를 이용하기 때문이다.

OIDC

OIDC 는 구글과 같은 IdP(ID 공급자)에 로그인 할 수 있도록 지원하는 프로토콜이다.

OAuth 2.0 프로토콜을 기반으로 만들어졌으며, OIDC 를 이용하면 외부 서비스를 통해 사용자 인증을 구현할 수 있다.

출처: Diving into IAM Roles for Service Accounts [aws]

위의 이미지는 파드에서 동작하는 애플리케이션이 AWS S3 버킷 목록을 가져오는 동작 흐름을 보여주는 예시이다.

  1. 파드에서 애플리케이션이 AWS SDK 를 이용해서 S3 버킷 목록을 요청한다. 이때, JWT 와 IAM 역할의 ARN 정보를 AWS STS 에게 전달한다.
  2. STS 는 AWS IAM 에게 임시 자격증명을 줄 수 있는 지 확인 요청한다.
  3. IAM 은 IAM OIDC Provider 와 통신해서 파드에 할당된 서비스 어카운트에 IAM 역할 정보가 잘 annotate 되어 있는 지 확인하고, IAM 에게 확인되었다는 응답을 준다.
  4. IAM 은 STS 에게 권한을 줘도 된다고 응답한다.
  5. STS 는 파드의 AWS SDK 에 임시 자격증명을 전달한다.
  6. 파드는 발급 받은 임시 자격증명을 이용해서 AWS SDK 는 S3 버킷 목록을 확인한다.

실습

IRSA 없이 AWS 리소스 접근

S3 버킷 목록을 가져오는 파드를 아래와 같이 실행했다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test1
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      args: ['s3', 'ls']
  restartPolicy: Never
  automountServiceAccountToken: false
  terminationGracePeriodSeconds: 0
EOF

하지만 로그를 확인해보면 Access Denied 라는 메시지와 함께 오류가 발생한 것을 확인할 수 있다.

kubectl logs eks-iam-test1
#An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

IRSA 생성 후 AWS 리소스 접근

아래의 명령어를 이용해서 AWS IAM 과 EKS 서비스 어카운트를 맵핑 해줄 수 있다.

eksctl create iamserviceaccount \
  --name my-sa \
  --namespace default \
  --cluster $CLUSTER_NAME \
  --approve \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)

my-sa 라는 이름의 서비스 어카운트는 S3 버킷 목록을 읽을 수 있는 정책과 연결되도록 했다.

아래의 명령어를 통해 EKS 서비스 어카운트가 생성되었고, 생성된 서비스 어카운트에 IAM 역할이 맵핑된 것을 확인할 수 있다.

eksctl get iamserviceaccount --cluster $CLUSTER_NAME
#NAMESPACE	NAME				ROLE ARN
#default		my-sa				arn:aws:iam::...:role/eksctl-myeks-addon-iamserviceaccount-default--Role1-0UCLwvCmtKaL

kubectl 명령어를 이용해도 서비스 어카운트가 생성된 것을 확인할 수 있다.

kubectl get sa
#NAME      SECRETS   AGE
#default   0         147m
#my-sa     0         22s

아래의 명령어를 실행해서 새로운 파드를 생성했다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test3
spec:
  serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

파드에서 S3 버킷 목록을 가져오는 명령어를 실행하면 정상적으로 가져오는 것을 확인할 수 있다.

kubectl exec -it eks-iam-test3 -- aws s3 ls
#2024-03-28 15:31:51 ...

만약, 정책에 연결되어 있는 S3 가 아닌 EC2 와 같은 서비스에 접근한다면 아래와 같이 오류가 발생한다.

kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region ap-northeast-2
#An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation. User: arn:aws:sts::265524074804:assumed-role/eksctl-myeks-addon-iamserviceaccount-default--Role1-0UCLwvCmtKaL/botocore-session-1713016405 is not authorized to perform: ec2:DescribeInstances because no identity-based policy allows the ec2:DescribeInstances action
#command terminated with exit code 254

Pod Identity

Pod Identity 는 IRSA 를 이용해서 IAM 권한을 최소한으로 부여해줄 수 있는 새로운 기능이다.

새로운 클러스터를 생성하면 OIDC Provider 링크가 달라지기 때문에 기존 역할 신뢰정책에 이를 추가해주어야 하는 번거로움이 있었는데, 이러한 문제를 해결하기 위해 등장한 기술이다.

OIDC 를 참조하지 않기 때문에 여러 EKS 클러스터에서 IAM 역할을 사용할 수 있다는 장점이 있다.

Pod Identity 는 노드에서 daemon set 으로 agent 를 실행한다.

agent 를 실행하기 위해서 EKS Pod Identity Agent add-on 을 설치해주기만 하면 된다.

출처: Amazon EKS Pod Identity: a new way for applications on EKS to obtain IAM credentials [aws]

실습

아래의 명령어로 add-on 을 설치할 수 있다.

eksctl create addon --cluster $CLUSTER_NAME \
--name eks-pod-identity-agent \
--version 1.2.0

아래의 명령어를 실행해서 S3 버킷 목록을 읽어오는 권한을 가진 pod identity association 을 생성할 수 있다.

eksctl create podidentityassociation \
--cluster $CLUSTER_NAME \
--namespace default \
--service-account-name s3-sa \
--role-name s3-eks-pod-identity-role \
--permission-policy-arns arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--region $AWS_REGION

서비스 어카운트는 생성해주지 않기 때문에 직접 생성해주었다.

kubectl create sa s3-sa

아래와 같이 파드를 생성해주었다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-pod-identity
spec:
  serviceAccountName: s3-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

S3 목록을 정상적으로 조회하는 것을 확인할 수 있다.

kubectl exec -it eks-pod-identity -- aws s3 ls
#2024-03-28 15:31:51 ...

Kyverno

Kyverno 는 쿠버네티스 리소스가 정책에 부합하는지 검사하고, 필요하면 리소스를 변경하고 생성하도록 도와주는 엔진이다.

Kyverno 는 그리스어로 “지배하다”라는 뜻을 가지고 있고, 발음은 ‘키베르노’ 라고 한다.

Kyverno 의 정책은 yaml 파일로 정의되고, 쿠버네티스 API 서버와 통신하며 정책을 적용한다.

출처: How Kyverno works [kyverno]

자세한 작동 원리는 이해를 못해서 실습한 내용만 정리했다.

실습

설치

모니터링을 이용하기 위해 아래의 yaml 파일을 생성했다. (참고: Montiroing [kyverno])

cat << EOF > kyverno-value.yaml
config:
  resourceFiltersExcludeNamespaces: [ kube-system ]

admissionController:
  serviceMonitor:
    enabled: true

backgroundController:
  serviceMonitor:
    enabled: true

cleanupController:
  serviceMonitor:
    enabled: true

reportsController:
  serviceMonitor:
    enabled: true
EOF

그라파나에서 대시보드를 이용하면 시각화해서 볼 수 있다. (대시보드 번호 15987)

아래의 명령어를 실행해서 kyverno 네임스페이스를 생성하고, helm 을 이용해서 설치했다.

kubectl create ns kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno --version 3.2.0-rc.3 -f kyverno-value.yaml -n kyverno

Validation 정책 적용

클러스터에 리소스를 생성할 때 team 이라는 label 이 반드시 존재하도록 설정하는 파일을 아래와 같은 yaml 파일로 정의했다.

kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "label 'team' is required"
      pattern:
        metadata:
          labels:
            team: "?*"
EOF

deployment 를 생성하기 위한 명령어를 실행했다.

kubectl create deployment nginx --image=nginx

하지만 아래와 같이 오류가 발생하는 것을 확인할 수 있다.

error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Deployment/default/nginx was blocked due to the following policies

require-labels:
  autogen-check-team: 'validation error: label ''team'' is required. rule autogen-check-team
    failed at path /spec/template/metadata/labels/team/'

label 에 team 을 붙여서 다시 생성해보자.

kubectl run nginx --image nginx --labels team=backend
#pod/nginx created

정상적으로 생성된 것을 확인할 수 있다.

kubectl get pod -l team=backend

#NAME    READY   STATUS    RESTARTS   AGE
#nginx   1/1     Running   0          19s

정책 삭제는 아래의 명령어를 이용해서 할 수 있다.

kubectl delete clusterpolicy require-labels

Mutating 정책 적용

쿠버네티스에 리소스를 생성할 때 label 에 team 이 없다면 team=bravo 를 추가해주는 정책을 생성했다.

kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-labels
spec:
  rules:
  - name: add-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            +(team): bravo
EOF

label 없이 파드를 생성하고 label 을 확인해보면 team=bravo 가 붙은 것을 확인할 수 있다.

kubectl run redis --image redis
kubectl get pod redis --show-labels
#NAME    READY   STATUS              RESTARTS   AGE   LABELS
#redis   0/1     ContainerCreating   0          3s    run=redis,team=bravo

label 을 붙인 채로 생성했다면 정책은 무시되는 것을 확인할 수 있다.

kubectl run newredis --image redis -l team=alpha
kubectl get pod newredis --show-labels
#NAME       READY   STATUS              RESTARTS   AGE   LABELS
#newredis   0/1     ContainerCreating   0          2s    team=alpha

마찬가지로 정책은 아래의 명령어를 이용해서 삭제할 수 있다.

kubectl delete clusterpolicy add-labels

Generation 정책 적용

새로운 리소스가 만들어질 때 함께 만들어질 수 있도록 하는 정책도 적용할 수 있다.

이번 예시에서는 새로운 네임스페이스를 생성하면 default 네임스페이스에 저장된 docker-registry 관련 시크릿을 복사하는 것을 다루었다.

default 네임스페이스에 아래와 같이 regcred 라는 이름을 가진 시크릿을 생성했다.

kubectl -n default create secret docker-registry regcred \
  --docker-server=myinternalreg.corp.com \
  --docker-username=john.doe \
  --docker-password=Passw0rd123! \
  --docker-email=john.doe@corp.com

Generation 정책을 생성하기 위해 아래와 같이 정의했다.

kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: sync-secrets
spec:
  rules:
  - name: sync-image-pull-secret
    match:
      any:
      - resources:
          kinds:
          - Namespace
    generate:
      apiVersion: v1
      kind: Secret
      name: regcred
      namespace: "{{request.object.metadata.name}}"
      synchronize: true
      clone:
        namespace: default
        name: regcred
EOF

네임스페이스가 새롭게 생성되면 regcred 라는 이름으로 시크릿을 생성하는데, default 네임스페이스에 존재하는 regcred 시크릿을 복사해오는 정책이다.

네임스페이스를 새롭게 생성하고 시크릿을 확인하면 regcred 라는 이름으로 생성되어있는 것을 확인할 수 있다.

kubectl create ns mytestns
kubectl -n mytestns get secret
#NAME      TYPE                             DATA   AGE
#regcred   kubernetes.io/dockerconfigjson   1      3s

default 네임스페이스에 저장된 secret 을 확인하는 명령어는 아래와 같다.

kubectl get secret regcred -n default --output="jsonpath={.data.\.dockerconfigjson}" \
| base64 --decode \
| jq

출력 결과는 아래와 같다.

{
  "auths": {
    "myinternalreg.corp.com": {
      "username": "john.doe",
      "password": "Passw0rd123!",
      "email": "john.doe@corp.com",
      "auth": "am9obi5kb2U6UGFzc3cwcmQxMjMh"
    }
  }
}

새롭게 생성한 네임스페이스인 mytestns 의 secret 은 아래의 명령어로 확인할 수 있다.

kubectl get secret regcred -n mytestns --output="jsonpath={.data.\.dockerconfigjson}" \
| base64 --decode \
| jq

출력 결과는 아래와 같다.

{
  "auths": {
    "myinternalreg.corp.com": {
      "username": "john.doe",
      "password": "Passw0rd123!",
      "email": "john.doe@corp.com",
      "auth": "am9obi5kb2U6UGFzc3cwcmQxMjMh"
    }
  }
}

default 네임스페이스에 정의된 secret 이 새로운 네임스페이스에 동일하게 생성된 것을 확인할 수 있다.

정책 삭제는 아래의 명령어를 실행했다.

kubectl delete clusterpolicy sync-secrets

후기

이번 주가 정말 큰 고비였던 거 같다.

다른 일정이 많아서 쿠버네티스에 많은 시간을 쏟을 수 없었고, 특히 인증과 인가는 내용이 다른 주제들보다 정말 복잡하고 알아야 하는 내용도 많았다.

솔직히 진짜 너무너무 어렵고 힘들었다.

스터디에서 다룬 내용을 전부 이해하진 못했지만, 현재 나의 수준에서 이해할 수 있는 정도에서 최선을 다해 정리했다.

그나마 이번 주 내용을 학습하며 이룬 큰 성취는 드디어 서비스 어카운트 개념을 이해했다는 것이다.

글만 읽었을 때는 전혀 이해가 안됐는데, 직접 가상머신에서 명령어를 실행하다보니 금방 이해할 수 있었다.

이번에 이해하지 못한 내용들은 나중에 다시 학습하면 깊이 이해할 수 있을 것 같다.

참고자료

profile
성장하는 개발자, 한준혁입니다.

0개의 댓글