[EKS@CloudNet] EKS Security

2024. 4. 14. 08:29EKS@CloudNet

- CloudNet에서 주관하는 EKS 스터디 2기 내용입니다

- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다

- 잘못된 점, 업데이트된 지식, 다른 코멘트 언제나 환영입니다!

 

 

 

0. Setup

 

- 기존에는 1 Bastion, 3 worker nodes 의 구조로 이루어졌지만 이번에는 2 bastions, 3 worker nodes로 구성된다. 한 Bastion은 하나의 유저에 맵핑되므로, 이번 실습 환경은 DevOps 신입사원이 한명 더 들어왔다는 가정의 구현이라고 볼 수 있다.

 

- 실습 환경은 지난번과 마찬가지로 가시다님의 원클릭 배포 파일을 cloudformation에서 수행하는 방식으로 세팅한다. kubeopsview, prometheus, grafana는 동일하니까 생략하겠다.

https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick5.yaml

 

 

 

 

1. k8s security

우선 k8s의 보안 정책인 인증과 인가에 대해서 간단히 알아보자. 이 과정은 kube-api 서버 접근을 통해 이뤄지는데,

인증 -> 인가 -> Admission Control 의 과정을 거쳐 이뤄진다.

 

인증(Authentication)

너는 누구니?

- CA cert, Client cert, Clienty key 3가지를 통해 인증한다

 

인가(Authorization)

그래서 너가 뭘 할 수 있니?

기본적으로 인증 이후에 일어난다.

- RBAC(Role-Based Account Control)

사용자(Role) 와 역할(Service Account)을 별도로 선언하고 이 둘을 조합해서(Role Binding) 해서 관리하므로 확장성이 좋다

 

 

실습

1. 개발팀과 인프라팀이 사용할 namespace 를 각각 만들고 namespace별로 SA 를 따로 만들어 준다

(SA 설정할 때 원하는 namespace에 있는지 꼭 확인한다!)

# create namespace
kubectl create namespace dev-team
kubectl create ns infra-team

# check namespace
kubectl get ns

# create SA
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team

# check SA
kubectl get sa -n dev-team
kubectl get sa -n infra-team

2. SA 를 지정하여 Pod 생성 하기

우선 각 namespace 에 kubectl pod를 새로 생성해준다. 이 kubectl은 1. 에서 만든 SA/Role이 적용되어 있다.

 

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-kubectl #infra-kubectl
  namespace: dev-team #infra-team
spec:
  serviceAccountName: dev-k8s #infra-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.28.5
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

 

- namespace 별로 SA 가 맵핑된 것을 볼 수 있다.

- token 자체를 확인할 필요가 있을 때는 아래와 같이 확인한다

kubectl exec -it dev-kubectl -n dev-team -- ls /run/secrets/kubernetes.io/serviceaccount
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/token
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/namespace
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/ca.crt

 

 

- 그러면 이렇게 만들어진 SA 의 권한을 테스트해보자. kubectl auth can-i 명령을 이용할 수 있다

alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'

k1 auth can-i get pods

현재는 권한이 없다고 나온다.

 

 

 

3. Role 을 이용한 권한 제어하기

Role 은 verb로 권한을 인가한다. 사용가능한 verb는 공식문서를 참고하자.

실습으로는 모든 권한을 가진 (wildcard로 *를 사용) role을 만든다.

# Role 만들기
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-dev-team #role-infra-team
  namespace: dev-team #infra-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

# Role 확인
kubectl get roles -n dev-team
kubectl describe roles role-dev-team -n dev-team

 

role이 잘 생성된 것을 확인한다

 

 

- 이번에는 Role과 SA를 연동하는 Rolebinding을 만들어 준다.

# Rolebinding 만들기
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-dev-team #roleB-infra-team
  namespace: dev-team #infra-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-dev-team #role-infra-team
subjects:
- kind: ServiceAccount
  name: dev-k8s #infra-k8s
  namespace: dev-team #infra-team
EOF

# Rolebinding 확인
kubectl get rolebindings -n dev-team
kubectl describe rolebindings roleB-dev-team -n dev-team

 

RB가 잘 생성되었고 SA와 Role이 연동되었다.

 

- 위에서와 같이 can-i명령을 사용해서 get pod 권한을 조회하면 이제는 가능하다.

 

- 다음 실습을 위해 namespace를 삭제한다

kubectl delete ns dev-team infra-team

 

 

 

 

2. EKS security

- 인증 절차는 AWS IAM 에서, 인가는 K8S RBAC 에서 진행한다.

왼쪽 그림은 IAM을 이용한 인증, 오른쪽 그림은 RBAC을 이용한 인가 과정을 설명한다

 

 

- RBAC 관련 유용한 krew plugin을 설치한다. 각각 뭔지 간단하게 짚고 넘어가면

kubectl krew install access-matrix rbac-tool rbac-view rolesum whoami

 

whoami: 인증된 주체를 확인한다

 

access-matrix: 현재 각 object type에 대해 가진 CRUD 권한을 시각화해준다

rbac-tool: User/groupd/sa의 이름으로 권한 범위와 role, rolebinding을 조회한다

rolesum: 가지고 있는 RBAC Role을 시각화해서 요약해 준다

 

 

 

실습

신입 Devops의 입사를 가정하고, myeks-bastion-2에 설정을 진행해 본다.

 

- 우선 bastion에서 IAM testuser을 생성하고 Admin 권한을 부여한다

# testuser 사용자 생성
aws iam create-user --user-name testuser

# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser

# testuser 사용자에 정책을 추가
aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser

# get-caller-identity 확인
aws sts get-caller-identity --query Arn

kubectl whoami #returns test-user

# EC2 IP 확인 : myeks-bastion-EC2-2 PublicIPAdd 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table

 

 

- 그러나 bastion2에서 접속해서 admin 권한이 필요한 동작을 하면

Unable to locate credentials. You can configure credentials by running "aws configure".

와 함께 거부당한다. 이때는 새로 접속한 Bastion-2에서 aws configure로 pub-priv key를 등록해주면 된다.

 

- 이제 bastion에서 testuser에게 system:masters 그룹을 부여해서 EKS 관리자 권한을 준다.

eksctl get iamidentitymapping --cluster $CLUSTER_NAME
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 | kubectl neat | yh

 

그러나 그래도 bastion2에는 권한이 없다.

 

 

- bastion2에서 testuser를 위한 Kubeconfig를 생성해준 후 Kubectl을 사용해야 한다.

# kubeconfig for testuser
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser

# kubectl 사용 확인
kubectl ns default
kubectl get node -v6

 

이제 Kubectl을 정상적으로 사용할 수 있다.

 

 

3. IRSA & Pod identity

IRSA: Iam Roles for Service Accounts

https://github.com/awskrug/security-group/blob/main/files/AWSKRUG_2024_02_EKS_ROLE_MANAGEMENT.pdf

- EC2 Instance Profile 은 매 Pod 마다 새로 role/SA를 만들어 주지 않는 이상 최소 권한 원칙에 위배된다. (작동은 가능하나 보안상 취약, 과연 가능하긴 한 걸까...?)

 

- 따라서 IRSA가 등장했다. 파드가 특정 IAM 역할로 Assume 할때 토큰을 AWS에 전송하고, AWS는 토큰과 EKS IdP(외부 Id Provider)를 통해 해당 IAM 역할을 사용할 수 있는지 검증하여 Pod 내부 어플리캐이션 컨테이너가 역할을 사용할 수 있게 해 준다.

 

 

 

실습

- EKS iam pod을 만들어 SA에 연결되는 IAM Role을 확인한다

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

# pod 확인
k describe pod eks-iam-test

 

이렇게 Pod을 만들고 나면 AWS console에서 Listbuckets 가 발생하는데 여기에서 이벤트 레코드를 볼 수 있다.

 

 

 

- 이번에는 JWT token을 실습해보자. JWT Token은 SA가 생성될 때 같이 생성되어 자동으로 Secret에 추가된다. JWT 토큰의 디코딩은 jwt.io 를 참고하자.

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

# SA token 확인
SA_TOKEN=$(kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $SA_TOKEN

 

이런 식으로 직접 token을 볼 수도 있다.

 

 

 

Pod identity

그러나 이런식으로 SA로 관리하면 모든 pod이 내려갔다 올라가고, 신규 클러스터를 만들게 되기 때문에 버전 업데이트가 굉장히 괴롭다고 한다.  따라서 addon인 eks-pod-identity-agent 를 사용해서 SA 로 활용되는 IAM credententials을 관리할 수 있다.

# 모니터링
watch -d kubectl get pod -A

# ADDON 선언
ADDON=eks-pod-identity-agent
aws eks describe-addon-versions \
    --addon-name $ADDON \
    --kubernetes-version 1.28 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text

# 설치
eksctl create addon --cluster $CLUSTER_NAME --name eks-pod-identity-agent --version 1.2.0

#  획인
eksctl get addon --cluster $CLUSTER_NAME

 

걸어놓은 모니터링에서 pod-identity-agent 가 2개 추가된 것을 볼 수 있다.

(이전)

(이후)

확인: pod-identity 말고 다른 addon 들도 실행되고 있다는 걸 알 수 있었고, pod-identity가 daemonset으로 구현되었음을 확인

eksctl get addon --cluster $CLUSTER_NAME
kubectl get ds -n kube-system eks-pod-identity-agent -o yaml | kubectl neat | yh

 

 

 

 

4. Kyverno

Kyverno는 K8s native 한 Policy manager로 정책을 자원으로 관리할 수 있게 해 준다. 타 언어 사용 없이 Custom Resource 만으로 정책을 관리한다. 이 중 Webhook에서 admission review 요청을 처리한다

 

Kyverno에 대해서는 아래 블로그들에 더 잘 설명되어 있다. 

- Devocean: kyverno 소개(OPA의 대항마?)

- 커피고래님 블로그

 

 

 

 

실습

- 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
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

# 확인
kubectl get all -n kyverno

 

+DEBUG: 실습 환경 세팅 시 Prometheus 를 설치하지 않은 상태로 Kyverno 설치를 시도하면 아래와 같은 에러메시지를 내면서 설치되지 않는다. 실습 환경 세팅 시 빼먹지 말고 Prometheus/Grafana 를 설치하자 (환경 세팅: 지난 포스팅 참고) 

Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: [resource mapping not found for name: "kyverno-admission-controller" namespace: "default" from "": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
ensure CRDs are installed first, resource mapping not found for name: "kyverno-background-controller" namespace: "default" from "": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
ensure CRDs are installed first, resource mapping not found for name: "kyverno-cleanup-controller" namespace: "default" from "": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
ensure CRDs are installed first, resource mapping not found for name: "kyverno-reports-controller" namespace: "default" from "": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
ensure CRDs are installed first]

 

- 이제는 kyverno 를 사용하기 위해 step-cli를 설치해준다.

wget https://dl.smallstep.com/cli/docs-cli-install/latest/step-cli_amd64.rpm
sudo rpm -i step-cli_amd64.rpm

 

 

- kyverno를 이용해 Secret을 조회해 보자

kubectl -n kyverno get secret
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d | step certificate inspect --short

 

가지고 있는 ca, crt, valid date를 확인할 수 있다.

- kyverno는 Prometheus/Grafana와도 연동해서 secret을 관리할 수 있다(아래참조).

 

 

 

- kyverno에서 validation을 진행해보기 위해 ClusterPolicy 를 새로 만든다.

# 모니터링
watch -d kubectl get pod -n kyverno

# ClusterPolicy 적용
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

# 확인
kubectl get validatingwebhookconfigurations
kubectl get ClusterPolicy

 

cleanup-admission-reports 와 cleanup-cluster-admissions 가 생성된 것을 확인한다

실행 후 grafana 에도 이렇게 새 cluster policy 와 rule 6개가 생성된 것을 확인한다.

+ scan success rate이 올라오기까지는 약 10분 정도 기다린 것 같다.

 

- 이번에는 deployment 생성을 시도하면서 validation이 되는지를 알아보자

우선 적용할 clusterpolicy 를 생성한다

# 모니터링
watch -d kubectl get pod -n kyverno

# ClusterPolicy 적용
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

# 확인
kubectl get validatingwebhookconfigurations
kubectl get ClusterPolicy

 

 

- 그리고 바로 deployment 생성을 시도하면 실패한다: ClusterPolicy.spec.validate.team 에 따르면 label로 team 이 명시되어 있지 않으면 실패하기 때문이다.

# 실패
kubectl create deployment nginx --image=nginx

# 성공
kubectl run nginx --image nginx --labels team=backend

# 확인
kubectl get policyreport -o wide

 

실행 후 확인해 보면, nginx Pod 만 Policyreport를 패스하는 것을 알 수 있다.

 

- rules에 Mutation을 추가할 수도 있다. Mutation과 validation의 차이는 달라도 같은 Field가 있으면 통과 되는 것이므로, team: bravo가 아니라 team: alpha 로도 생성 가능하다. 만약 label이 명시되어 있지 않으면 default로 하나 더 생성된다

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

# 확인
kubectl get mutatingwebhookconfigurations
kubectl get ClusterPolicy
NAME         ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE     MESSAGE
add-labels   true        true         Audit             True    6m41s   Ready

# 파드 생성 후 label 확인
kubectl run redis --image redis
kubectl get pod redis --show-labels

# 파드 생성 후 label 확인 : 바로 위와 차이점은?
kubectl run newredis --image redis -l team=alpha
kubectl get pod newredis --show-labels

 

 

 

 

 

잊지 말아요 자원 삭제

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

+ 생성된 IAM user 는 웹 콘솔에서 삭제해준다.

 

 

 

 

이번에는 개념적으로 알아야 할 분량이 많아서 카테고리별로 실습을 맵핑해서 만들었는데 가독성이 어떤지 모르겠다..! 아직도 나 확실히 잘안다! 최고다! 까지는 아니지만 이번 세션을 통해 막막했던 보안에 대해 부담을 덜 수 있었다.