이번 게시글은 가시다님의 AEWS [2기] 스터디 내용을 정리한 포스트 입니다.
이번 게시글은 6 주차의 스터디 내용인 EKS Security에 대해 살펴봅니다.
EKS 배포
실습을 위해 EKS 클러스터를 배포합니다. AWS 콘솔에 로그인 한 뒤 CloudFormation에서 아래 YAML 파일로 스택을 생성하시면 됩니다. 진행 과정은 아래 링크를 참고해 주세요.
[EKS] Networking
이번 게시글은 가시다님의 AEWS [2기] 스터디 내용을 정리한 포스트 입니다. 이번 게시글은 2 주차의 스터디 내용인 EKS Networking에 대해 살펴봅니다. EKS 원클릭 배포 사전 준비 AWS 계정 SSH 키 페어 IA
junkmm.tistory.com
배포 확인
Bastion EC2 인스턴스의 공인 IP로 SSH 접근을 시도합니다. 공인 IP는 CloudFormation의 결과에 나오는 출력값 혹은 EC2 메뉴에서 bastion 호스트의 공인 IP를 확인합니다.
# ssh -i "Keyname" ec2-user@"Public IP"
ssh -i hj42700eks.pem ec2-user@3.36.133.15
Bastion 접속 후 아래와 같이 기본 설정을 진행합니다.
# default 네임스페이스 적용
kubectl ns default
# 노드 정보 확인 : t3.medium
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
# ExternalDNS
MyDomain=<자신의 도메인>
echo "export MyDomain=<자신의 도메인>" >> /etc/profile
MyDomain=junkmm.site
echo "export MyDomain=junkmm.site" >> /etc/profile
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"
# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
--set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
# gp3 스토리지 클래스 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml
# 노드 IP 확인 및 PrivateIP 변수 지정
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3
# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.200/32
# 워커 노드 SSH 접속
for node in $N1 $N2 $N3; do ssh -o StrictHostKeyChecking=no ec2-user@$node hostname; done
for node in $N1 $N2 $N3; do ssh ec2-user@$node hostname; done
프로메테우스 & 그라파나(admin / prom-operator) 설치 : 대시보드 추천 15757 17900 15172
# 사용 리전의 인증서 ARN 확인
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN
# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
cat <<EOT > monitor-values.yaml
prometheus:
prometheusSpec:
podMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelectorNilUsesHelmValues: false
retention: 5d
retentionSize: "10GiB"
ingress:
enabled: true
ingressClassName: alb
hosts:
- prometheus.$MyDomain
paths:
- /*
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/ssl-redirect: '443'
grafana:
defaultDashboardsTimezone: Asia/Seoul
adminPassword: prom-operator
defaultDashboardsEnabled: false
ingress:
enabled: true
ingressClassName: alb
hosts:
- grafana.$MyDomain
paths:
- /*
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/ssl-redirect: '443'
alertmanager:
enabled: false
EOT
cat monitor-values.yaml | yh
# 배포
kubectl create ns monitoring
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 57.2.0 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring
# Metrics-server 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 프로메테우스 ingress 도메인으로 웹 접속
echo -e "Prometheus Web URL = https://prometheus.$MyDomain"
# 그라파나 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"
Kubernetes 인증/인가
Overview | 아래 그림은 kubernetes API를 호출하는 과정을 도식화 한 것입니다. 사용자는 `kubectl`같은 cli 도구로 `kubectl delete namespace demo`와 같은 명령어를 전송할 수 있습니다. 명령을 전달받은 API는 첫 째, 인증/인가를 수행합니다. 이 요청자의 신원을 파악하고, 신원이 파악되었다면 demo namespace를 지울 수 있는지에 대한 권한을 확인합니다. 추 후 validating admission는 demo namespace가 Terminating 중일 때 다른 사용자가 demo namespace에 리소스를 생성하지 못하도록 제약을 걸 수 있습니다.
For example, when a namespace is deleted and subsequently enters the Terminating state, the NamespaceLifecycle admission controller is what prevents any new objects from being created in this namespace.
https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/
User Account의 종류 | 아래 그림은 Kubernetes Api-server를 호출할 수 있는 User Account 종류에 대해 설명하고 있습니다. 사용자(제가)가 가장 많이 사용하는 kubectl로도 API를 호출할 수 있고, 별도의 Kubernetes Dashboard, 혹은 Service Account를 생성하고, 특정 리소스에 할당하여 Kubernetes API를 호출할 수 있습니다.
인증 | 아래 그림은 Kubernetes의 인증(Authentication)처리 방법에 대해 설명하고 있습니다. 크게 X.509 Client Certs, kubectl, Service Account 세 가지가 존재합니다.
X.509 Client Certs 방식은 kubeconfig에 CA crt(발급 기관 인증서), Client crt(클라이언트 인증서), Client key(클라이언트 개인키)를 통해 인증하는 방식입니다. 호출 방식은 아래와 같습니다. kubectl이 아닌 `curl`을 통해 API를 호출합니다.
# 출처 : https://coffeewhale.com/kubernetes/authentication/x509/2020/05/02/auth01/
# API 서버 주소 및 포트 설정
API_SERVER_ADDR=XXXX # 예시) localhost
API_SERVER_PORT=XXX # 예시) 6443
# kubectl - 신규 X.509 사용자 인증
curl --cacert k8s-rootCA.pem --cert k8s-new-client.pem --key k8s-new-client-key.pem https://$API_SERVER_ADDR:$API_SERVER_PORT/api
# kubectl - 신규 X.509 사용자 인증
kubectl --kubeconfig=$HOME/kubeconfig get pod -n kube-system
kubectl 방식은 사용자 cli를 통해 API를 호출하는 것으로 보통 `~/.kube/config`에 기재된 kubernetes 접속, 인증 정보를 기반으로 API를 호출합니다.
Service Account는 파드의 일부 컨테이너에서 실행되는 애플리케이션 프로세스를 위한 것으로, Token을 사용해 API를 호출할 수 있습니다.
인가 | 인가의 경우 인증을 통해 식별된 안전한 사용자가 수행하는 행위(create, delete 등)에 대해 제어합니다. RBAC방식으로 이를 구현합니다. RBAC은 역할 기반의 권한 관리로, 사용자와 역할을 별개로 선언한 뒤 두가지를 조합(binding)하여 사용자에게 권한을 부여하는 방식으로 kubectl 혹은 API로 관리할 수 있습니다.
간단한 실습으로 인증/인가를 확인해 보겠습니다. dev, infra user, dev, infra namespace를 생성한 뒤 각 사용자는 본인의 namespace에 대해서만 제어할 수 있도록 구성해봅니다.
먼저 dev-team, infra-team 이름의 네임스페이스와 서비스 어카운트를 생성합니다.
# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team
# 네임스페이스 확인
kubectl get ns
---
NAME STATUS AGE
default Active 104m
dev-team Active 3s
infra-team Active 2s
kube-node-lease Active 104m
kube-public Active 104m
kube-system Active 104m
monitoring Active 73m
# 네임스페이스에 각각 서비스 어카운트 생성 : 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
---
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2024-04-11T11:23:06Z"
name: dev-k8s
namespace: dev-team
resourceVersion: "29441"
uid: 904ccd47-9588-42cc-a73a-b7d59563652a
kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml | yh
---
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2024-04-11T11:23:07Z"
name: infra-k8s
namespace: infra-team
resourceVersion: "29446"
uid: 253d7d10-29d2-4f21-90a6-89979f528448
그 다음 각 네임스페이스에 kubectl 명령을 사용할 수 있는 Pod를 배포합니다. 이 때 `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
# 확인
kubectl get pod -A
kubectl get pod -o dev-kubectl -n dev-team -o yaml
serviceAccount: dev-k8s
...
kubectl get pod -o infra-kubectl -n infra-team -o yaml
serviceAccount: infra-k8s
...
# 파드에 기본 적용되는 서비스 어카운트(토큰) 정보 확인
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
# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'
# 권한 테스트
k1 get pods # kubectl exec -it dev-kubectl -n dev-team -- kubectl get pods 와 동일한 실행 명령이다!
k1 run nginx --image nginx:1.20-alpine
k1 get pods -n kube-system
k2 get pods # kubectl exec -it infra-kubectl -n infra-team -- kubectl get pods 와 동일한 실행 명령이다!
k2 run nginx --image nginx:1.20-alpine
k2 get pods -n kube-system
# (옵션) kubectl auth can-i 로 kubectl 실행 사용자가 특정 권한을 가졌는지 확인
k1 auth can-i get pods
no
`k1 get pods -n kube-system` 시 아래와 같이 dev-team 서비스 어카운트는 kube-system 네임스페이스에 pods의 list를 확인해볼 수 없다는 오류가 발생합니다.(forbidden)
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "pods" in API group "" in the namespace "kube-system"
command terminated with exit code 1
그럼 아래와 같이 각각 네임스페이스에 롤(Role)을 생성한 뒤 서비스 어카운트에 바인딩 하고, 결과를 확인 해 보겠습니다. 여기서 롤(Role,역할)은 apiGroups와 resources로 지정된 리소스에 대해 verbs 권한을 인가 해줌을 정의하고, verbs에는 *(모두 처리), create(생성), delete(삭제), get(조회), list(목록조회), patch(일부업데이트), update(업데이트), watch(변경감시) 옵션이 있습니다.
아래 코드에선 Role 정의 후 Role Binding을 진행합니다.
# 각각 네임스페이스내의 모든 권한에 대한 롤 생성
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
# 롤 확인
kubectl get roles -n dev-team
kubectl get roles -n infra-team
kubectl get roles -n dev-team -o yaml
kubectl describe roles role-dev-team -n dev-team
...
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
# 롤바인딩 생성 : '서비스어카운트 <-> 롤' 간 서로 연동
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
# 롤바인딩 확인
kubectl get rolebindings -n dev-team
kubectl get rolebindings -n infra-team
kubectl get rolebindings -n dev-team -o yaml
kubectl describe rolebindings roleB-dev-team -n dev-team
...
Role:
Kind: Role
Name: role-dev-team
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount dev-k8s dev-team
서비스 어카운트를 지정한 뒤 파드에서 다시 권한을 테스트 해봅니다.
# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'
# 권한 테스트
k1 get pods
k1 run nginx --image nginx:1.20-alpine
k1 get pods
k1 delete pods nginx
k1 get pods -n kube-system
k1 get nodes
k2 get pods
k2 run nginx --image nginx:1.20-alpine
k2 get pods
k2 delete pods nginx
k2 get pods -n kube-system
k2 get nodes
# (옵션) kubectl auth can-i 로 kubectl 실행 사용자가 특정 권한을 가졌는지 확인
k1 auth can-i get pods
yes
dev Role에서 namespace를 dev-team으로 지정했기 때문에 아래와 같이 dev-pod에서는 dev-team 네임스페이스에 대한 리소스 조회, 생성이 가능하지만, 이 외 네임스페이스에 대해선 동일하게 forbidden 권한 오류가 발생하며 접근하지 못하는 것을 확인해볼 수 있습니다.
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# k1 get pods -n dev-team
NAME READY STATUS RESTARTS AGE
dev-kubectl 1/1 Running 0 17m
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# k1 get pods -n infra-team
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "pods" in API group "" in the namespace "infra-team"
command terminated with exit code 1
만약 서비스어카운트에 여러 네임스페이스에 대한 역할을 부여하고 싶다면, Role 대신 ClusterRole을 사용해야 합니다.
아래 명령으로 실습 리소스를 삭제합니다.
kubectl delete ns dev-team infra-team
EKS 인증/인가
사용자는 EKS 클러스터에 kubectl 명령을 전송하면, 인증은 AWS IAM에서 처리하고, 인가는 Kubernetes RBAC에서 제어됩니다.
우선 EKS RBAC 분석을 용이하게 하기 위한 krew 플러그인 설치를 진행합니다.
# 설치
kubectl krew install access-matrix rbac-tool rbac-view rolesum whoami
# k8s 인증된 주체 확인
kubectl whoami
arn:aws:iam::6546...:user/hakjunadmin
`access-matrix`는 EKS 리소스에 대한 권한을 확인할 수 있습니다.
# Show an RBAC access matrix for server resources
kubectl access-matrix # Review access to cluster-scoped resources
kubectl access-matrix --namespace default # Review access to namespaced resources in 'default'
![](https://blog.kakaocdn.net/dn/b9IPG4/btsGzZZpaUi/KJWLUOxqr359ytagVqlldk/img.png)
`rback-tool lookup`은 사용자 별 역할을 확인해볼 수 있습니다.
# RBAC Lookup by subject (user/group/serviceaccount) name
kubectl rbac-tool lookup
kubectl rbac-tool lookup system:masters
SUBJECT | SUBJECT TYPE | SCOPE | NAMESPACE | ROLE
+----------------+--------------+-------------+-----------+---------------+
system:masters | Group | ClusterRole | | cluster-admin
kubectl rbac-tool lookup system:nodes # eks:node-bootstrapper
kubectl rbac-tool lookup system:bootstrappers # eks:node-bootstrapper
kubectl describe ClusterRole eks:node-bootstrapper
![](https://blog.kakaocdn.net/dn/ceUqNn/btsGyS1hwCx/zFNkIbfcZCCPPAeMHSXqWk/img.png)
`kubectl rbac-tool whoami`는 현재 사용자 정보에 대해 출력합니다.
![](https://blog.kakaocdn.net/dn/DepDY/btsGCcryn3F/AUKq9oXqOZRaCktkkTsH00/img.png)
`kubectl rolesum`은 서비스어카운트, 사용자, 그룹에 대한 RBAC 정보를 요약하여 알려줍니다.
kubectl rolesum -h
kubectl rolesum aws-node -n kube-system
kubectl rolesum -k User system:kube-proxy
kubectl rolesum -k Group system:masters
kubectl rolesum -k Group system:authenticated
![](https://blog.kakaocdn.net/dn/bNLT0L/btsGwB7uWK1/9XbithslezXQCT4enLGaS0/img.png)
`kubectl rbac-view`는 RBAC 권한을 웹 페이지로 시각화하여 제공합니다.
![](https://blog.kakaocdn.net/dn/8KBAT/btsGxAfLruF/mAVYD9kqnikA3KcfIhbzKK/img.png)
![](https://blog.kakaocdn.net/dn/dQ5qYS/btsGySGZfVA/xN0MrMWQI5zSfrtaHrA3uK/img.png)
EKS 인증/인가 부분을 자세히 확인해 보겠습니다.
1. 사용자가 kube-api를 호출할 때 BearerToken(Pre-signed URL)을 함께 전달합니다.
2. kube-api는 admission과 연동된 Webhok Token Authentication에, Token Review를 요청합니다.
3. Webhok은 IAM에 sts GetCallerIdentity를 요청합니다.
4. IAM은 사용자 정보를 Webhok에 전달합니다.
5. Webhok은 IAM으로부터 전달받은 사용자 정보를 aws-auth Configmaps에 확인을 요청합니다.
6. IAM, Configmaps로 부터 사용자 인증이 완료되면, kube-api는 해당 사용자에 대한 권한을 확인합니다.
7. 사용자 보유 권한 내 요청이 확인되면 요청을 승인합니다.
위 과정을 상세히 확인해보겠습니다. 먼저 Token입니다. 아래 aws cli를 입력하면 Token값을 반환 합니다. Token은 만료 기간이 있습니다. 기간이 만료되면 토큰이 노출되더라도 실제 사용은 불가능합니다.
aws eks get-token --cluster-name $CLUSTER_NAME | jq
Bastion 호스트에서 `cat ~/.kube/config` 명령으로 kubeconfig 파일을 확인 해 보겠습니다. kubectl을 요청할 때 cluster의 endpoint를 확인할 수 있고, 하단 users 정보에는 위에서 사용한 `aws eks get-token`명령을 사용하는것을 확인할 수 있습니다. 따라서 kubectl 명령을 보낼때, Token을 발급받아 사용하는 것임을 알 수 있습니다.
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: L...K
server: https://A4.gr7.ap-northeast-2.eks.amazonaws.com
name: myeks.ap-northeast-2.eksctl.io
contexts:
- context:
cluster: myeks.ap-northeast-2.eksctl.io
user: hakjunadmin@myeks.ap-northeast-2.eksctl.io
name: hakjunadmin@myeks.ap-northeast-2.eksctl.io
current-context: hakjunadmin@myeks.ap-northeast-2.eksctl.io
kind: Config
preferences: {}
users:
- name: hakjunadmin@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
provideClusterInfo: false
아래 그림은 API 호출 예제라고 합니다.
위에서 발급한 토큰을 jwt.io 페이지에서 디코드를 진행 해보면, 아래와 같은 DATA가 확인되는것을 확인할 수 있습니다.
"https://sts.ap-northeast-2.amazonaws.com/?
Action=GetCallerIdentity&
Version=2011-06-15&
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=AK%2Fap-northeast-2%2Fsts%2Faws4_request&
X-Amz-Date=20240412T144854Z&
X-Amz-Expires=60&
X-Amz-SignedHeaders=host%3Bx-k8s-aws-id&
X-Amz-Signature=0a4"
위 API호출을 받은 EKS API는 Token Review를 Webhook token authenticator에 요청합니다. AWS IAM은 해당 호출을 인증 완료한 뒤 User/Role에 대한 ARN을 반환합니다.
쿠버네티스 RBAC 인가는 IAM User/Role이 확인되면 k8s aws-auth configmaps에서 mapping 정보를 확인하게 됩니다. aws-auth 컨피그맵에 IAM 사용자, 역할ARN, K8S 오브젝트로 권한을 확인한 뒤 인가 허가가 되면 최종적으로 동작을 수행합니다.
참고로 EKS를 생성한 IAM principal은 aws-auth와 상관없이 kubernetes-admin Username으로 system:masters 그룹에 권한을 가집니다.
# Webhook api 리소스 확인
kubectl api-resources | grep Webhook
mutatingwebhookconfigurations admissionregistration.k8s.io/v1 false MutatingWebhookConfiguration
validatingwebhookconfigurations admissionregistration.k8s.io/v1 false ValidatingWebhookConfiguration
# validatingwebhookconfigurations 리소스 확인
kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
aws-load-balancer-webhook 3 70m
eks-aws-auth-configmap-validation-webhook 1 93m
kube-prometheus-stack-admission 1 68m
vpc-resource-validating-webhook 2 93m
kubectl get validatingwebhookconfigurations eks-aws-auth-configmap-validation-webhook -o yaml | kubectl neat | yh
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: eks-aws-auth-configmap-validation-webhook
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJV3NiR1dNRFN1c3d3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBME1USXhOREE1TVRSYUZ3MHpOREEwTVRBeE5ERTBNVFJhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNpak9CRGloaXFNSkVOdlp4VHFWZEJmZERJN090dGtjWXhvbnlOcjlzbjJ3c3loNVR5cFdpbSt0eDYKZTQrQnZ5ZkRpMGk0bXVCVzhHZm9PZFovZFQ0d3ZOckl4bFYxckNEOE9XRUNPNWQyR29QeGs2RWlHUitsVzlqZgpXL1BWWHVMa0UvVHVJYUlRaS8vT09OU3Y2SnNkdW0yQUdwaDRvdjl5cmcwa1dxTy9LdVdmUEJwelBya3BsTjhuCkJ2ZmZMSXpBZFpLOEFJdzlyOE1PZ05vVnNqd0hnalBzU0R6cGpqck9YZWZaZ3p3ZFZHZEFaTDlCNWV4cTFKeU8KWHZkbHA0Lytnb3djcTZtMWw1WDJhQ1l3MTByUlVIRTMwdjM5U21zaEJFajFkTG0vNWVUWDBuL1FheVlzYXN5RApNTVcxbnBFNS9WekFOVm5JdHBrTlZ5OXRzVXhqQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJTaWFKU2x6TUV4ZG9QSDhWcWdZYnNoV0dYVkJUQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQldKNHlGV1ZobgpUNlFHTmZ3bC9ybHNYMEZtU2FkT1JnWCtjaDR6dUlaZUNoY3Ziek1wbVNNUEhyZXdBWGhTUnZIc2hxeDdIbTRoCmxNeWE1SGRrajllOVdlNCt2a3ZuZkY1Y3FjODBybnNJczVTM2hVMDhRNnhVRFpGMmVPY1J4RURtWlovcEk4bHYKSFl4U1ZsRGtycWdYVHhNbzNnZVVCenlPVm1telVUa1BBWGM2QmVESHhKZUVDZ1kvWFVYVERGK0JPaW9OMDhCVgozVW56QTd3bzdtNHA4WGsrOWtpMm81SkJVZmo1MDVNSkxxdEpFVlgvQVQ0MXhxOHpSQThNQVhmZnZMNENEYWFzCmpmMXc2aHZMNUlwejluTDUwZjMxdDRTTFAvcEVYOWU1TXlwb0lmZWwrMVZwblpQTmpBQnpzM1VaSldXRG9BeUIKMktMZXQvdVdCMk4wCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
url: https://127.0.0.1:21375/validate
failurePolicy: Ignore
matchConditions:
- expression: (request.name == "aws-auth")
name: aws-auth-configmap
matchPolicy: Equivalent
name: eks-aws-auth-configmap-validation-webhook.amazonaws.com
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
resources:
- configmaps
scope: '*'
sideEffects: None
timeoutSeconds: 5
# aws-auth 컨피그맵 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::6546:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-nexe8GYoQa63
username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
#---<아래 생략(추정), ARN은 EKS를 설치한 IAM User , 여기 있었을경우 만약 실수로 삭제 시 복구가 가능했을까?---
mapUsers: |
- groups:
- system:masters
userarn: arn:aws:iam::111122223333:user/admin
username: kubernetes-admin
# EKS 설치한 IAM User 정보 >> system:authenticated는 어떤 방식으로 추가가 되었는지 궁금???
{Username: "arn:aws:iam::6546:user/hakjunadmin",
UID: "aws-iam-authenticator:6546:AI",
Groups: ["system:authenticated"],
Extra: {accessKeyId: ["AKIAZQ3DNPORYDDXLK27"],
arn: ["arn:aws:iam::6546:user/hakjunadmin"],
canonicalArn: ["arn:aws:iam::6546:user/hakjunadmin"],
principalId: ["AIDAZQ"],
sessionName: [""]}}
...
# system:masters , system:authenticated 그룹의 정보 확인
kubectl rbac-tool lookup system:masters
kubectl rbac-tool lookup system:authenticated
kubectl rolesum -k Group system:masters
kubectl rolesum -k Group system:authenticated
# system:masters 그룹이 사용 가능한 클러스터 롤 확인 : cluster-admin
kubectl describe clusterrolebindings.rbac.authorization.k8s.io cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
Role:
Kind: ClusterRole
Name: cluster-admin
Subjects:
Kind Name Namespace
---- ---- ---------
Group system:masters
# cluster-admin 의 PolicyRule 확인 : 모든 리소스 사용 가능!
kubectl describe clusterrole cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
[*] [] [*]
# system:authenticated 그룹이 사용 가능한 클러스터 롤 확인
kubectl describe ClusterRole system:discovery
kubectl describe ClusterRole system:public-info-viewer
kubectl describe ClusterRole system:basic-user
kubectl describe ClusterRole eks:podsecuritypolicy:privileged
그럼 데브옵스 신입 사원을 위한 myeks-bastion-2에 설정을 진행해보겠습니다. aws iam user를 생성하고, EKS 클러스터에 접근할 수 있도록 구성해 봅니다. 현재 Bastion에서 IAM 유저를 생성하고, Access Key 발급 및 권한 부여를 합니다.
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# aws iam create-user --user-name testuser
{
"User": {
"Path": "/",
"UserName": "testuser",
"UserId": "###",
"Arn": "arn:aws:iam::###:user/testuser",
"CreateDate": "2024-04-12T15:53:36+00:00"
}
}
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# aws iam create-access-key --user-name testuser
{
"AccessKey": {
"UserName": "testuser",
"AccessKeyId": "###",
"Status": "Active",
"SecretAccessKey": "###+###",
"CreateDate": "2024-04-12T15:53:42+00:00"
}
}
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser
Bastion2 호스트에 접속 후 testuser의 AccessKey로 aws configure를 진행합니다.
testuser가 kubectl 명령을 사용할 수 있게 설정합니다.
eksctl create iamidentitymapping --cluster $CLUSTER_NAME --username testuser --group system:masters --arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws-auth를 확인해보면 아래와 같이 추가된 것을 확인할 수 있습니다.
mapUsers: |
- groups:
- system:masters
userarn: arn:aws:iam::##:user/testuser
username: testuser
Bastion2에서 아래 명령으로 Kubeconfig를 생성합니다.
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser
Bastion2 에서도 kubectl명령을 사용할 수 있게 되었습니다.
configmap의 중요성을 확인해보기 위해 aws-auth에 매핑된 그룹을 바꿔보겠습니다.
kubectl edit cm -n kube-system aws-auth
system:authenticated으로 수정
수정 뒤 아래와 같이 권한이 없어 조회가 불가능함을 확인합니다.
아래 명령으로 IAM 맵핑을 삭제할 수도 있습니다. 아래 명령을 수행하면 aws-auth 컨피그맵에 testuser의 내용이 삭제됩니다.
eksctl delete iamidentitymapping --cluster $CLUSTER_NAME --arn arn:aws:iam::$ACCOUNT_ID:user/testuser
EC2 Instance Profile | (IAM Role)에 매핑된 K8S RBAC 확인해보기, 최근 추가된 기능
노드에 매핑관 Role을 확인해봅니다. 워커노드 각각 별도의 Role을 사용하고 있음을 확인합니다.
# 노드에 STS ARN 정보 확인 : Role 뒤에 인스턴스 ID!
for node in $N1 $N2 $N3; do ssh ec2-user@$node aws sts get-caller-identity --query Arn; done
"arn:aws:sts::65:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-nexe8GYoQa63/i-051140852d9fddf15"
"arn:aws:sts::65:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-nexe8GYoQa63/i-038360583545cfdae"
"arn:aws:sts::65:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-nexe8GYoQa63/i-006b15c6dca84673f"
aws-auth 컨피그맵을 확인해보면, mapRoles: 필드에서 rolearn:으로 인스턴스에 할당된 Role에 대해 사용자 인증을 해주고 있는것을 확인할 수 있습니다.
kubectl describe configmap -n kube-system aws-auth
그럼 워커노드에 할당된 역할 정보를 불러와 aws cli를 사용하는 파드를 배포한 뒤 aws cli를 작동해 보겠습니다.
# 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
# 파드 생성 확인
kubectl get pod -owide
# 파드 이름 변수 지정
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)의 ARN 정보 확인
kubectl exec -it $APODNAME1 -- aws sts get-caller-identity --query Arn
kubectl exec -it $APODNAME2 -- aws sts get-caller-identity --query Arn
# awscli 파드에서 EC2 InstanceProfile(IAM Role)을 사용하여 AWS 서비스 정보 확인 >> 별도 IAM 자격 증명이 없는데 어떻게 가능한 것일까요?
# > 최소권한부여 필요!!! >>> 보안이 허술한 아무 컨테이너나 탈취 시, IMDS로 해당 노드의 IAM Role 사용 가능!
kubectl exec -it $APODNAME1 -- aws ec2 describe-instances --region ap-northeast-2 --output table --no-cli-pager
kubectl exec -it $APODNAME2 -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
# EC2 메타데이터 확인 : IDMSv1은 Disable, IDMSv2 활성화 상태, IAM Role - 링크
kubectl exec -it $APODNAME1 -- bash
-----------------------------------
아래부터는 파드에 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
# 위에서 출력된 IAM Role을 아래 입력 후 확인
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-nexe8GYoQa63
{
"Code" : "Success",
"LastUpdated" : "2024-04-12T16:13:46Z",
"Type" : "",
"AccessKeyId" : "",
"SecretAccessKey" : "",
"Token" : "",
"Expiration" : "2024-04-12T22:25:02Z"
}
## 출력된 정보는 AWS API를 사용할 수 있는 어느곳에서든지 Expiration 되기전까지 사용 가능
# 파드에서 나오기
exit
---
기존에 권한 관리를 쉽게 적용한 방법
AWS EKS 액세스 구성모드가 EKS API 및 ConfigMap 이면 정책 중복 시 EKS API가 우선시 된다. 결론적으로 Configmap보다 EKS API를 사용하게 되며, 최근들어 API 사용을 권장한다고 합니다.
EKS API 호출 방법을 확인해보겠습니다.
aws eks update-cluster-config --name $CLUSTER_NAME --access-config authenticationMode=API
액세스가 EKS API 인증모드로 변경되었습니다.
기존에 EKS를 생성한 User가 EKS클러스터에 접근 가능한 이유는 해당 User에 대한 액세스 정책으로 `AmazonEKSClusterAdminPolicy`를 보유하고 있기 때문입니다.
액세스 정책 별 접근 가능한 권한(크게 4 가지)은 링크에서 확인 가능합니다.
# 맵핑 클러스터롤 정보 확인
kubectl get clusterroles -l 'kubernetes.io/bootstrapping=rbac-defaults' | grep -v 'system:'
그럼 testuser를 API방식으로 사용하기위한 설정을 적용합니다.
# testuser 의 access entry 생성
aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
{
"accessEntry": {
"clusterName": "myeks",
"principalArn": "arn:aws:iam::6:user/testuser",
"kubernetesGroups": [],
"accessEntryArn": "arn:aws:eks:ap-northeast-2:65:access-entry/myeks/user/65/testuser/54c76c1d-e41a-812d-3347-fad17fdffab8",
"createdAt": "2024-04-14T01:13:00.394000+09:00",
"modifiedAt": "2024-04-14T01:13:00.394000+09:00",
"tags": {},
"username": "arn:aws:iam::654654143395:user/testuser",
"type": "STANDARD"
}
}
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]
arn:aws:iam::654654143395:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-c1AQaMSrbK4x
arn:aws:iam::654654143395:user/hakjunadmin
arn:aws:iam::654654143395: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
{
"clusterName": "myeks",
"principalArn": "arn:aws:iam::654:user/testuser",
"associatedAccessPolicy": {
"policyArn": "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
"accessScope": {
"type": "cluster",
"namespaces": []
},
"associatedAt": "2024-04-14T01:13:14.146000+09:00",
"modifiedAt": "2024-04-14T01:13:14.146000+09:00"
}
}
#
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
Bastion2에서 testuser로 kubectl이 가능한것을 확인할 수 있습니다.
EKS API를 사용해 IAM User를 액세스 정책과 연동해 EKS 연결방법을 확인 해 보았습니다. 아래 명령으로 access entry를 제거합니다.
aws eks delete-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
이번에는 미리 만들어진 정책을 사용하지 않고, 사용자 지정 Role을 매핑하여 적용하는 것을 실습해보겠습니다. viewr, admin role 2개를 생성합니다.
#
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
#
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
test user를 viewer와 연결합니다.
aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser --kubernetes-group pod-viewer
test user는 kubectl get pod 는 가능하지만 delete pod는 불가능 합니다.
testuser의 권한을 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 delete가 가능해 집니다.
EKS IRSA & Pod Identitty
EKS의 Pod가 AWS S3등 AWS 리소스를 사용할 때 AWS STS/IAM과 IAM OIDC를 사용해 인증/인가를 처리하는 방식입니다. 기존 인스턴스 프로필, 액세스 키 사용시 키 노출로 인한 보안 허점을 보완할 수 있는 방법입니다.
우선 Pod의 Token 처리 방법을 확인해 보겠습니다.
# 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
IRSA는 파드에 할당하여 사용하는 서비스 어카운트입니다. 파드에 Role이 매핑되어 있어 최소권한부여를 만족할 수 있습니다.
상세 과정을 도식화 하면 아래와 같습니다.
실습으로 내용을 확인해 보겠습니다. 먼저 파드를 생성하고 서비스 어카운트는 별도 지정해보지 않도록 구성해보겠습니다. 아래 파드는 s3 ls 명령을 입력하는 파드입니다.
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
`automountServiceAccountToken`이 `false`로 설정되어 있어 Pod에 서비스어카운트를 별도 할당하지 않게 됩니다. 권한이 없어, Cloud Trail에서 Access Denied가 발생한 것을 확인할 수 있습니다.
두 번째 파드를 생성해보겠습니다.
# 파드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
# 확인
kubectl get pod
kubectl describe pod
kubectl get pod eks-iam-test2 -o yaml | kubectl neat | yh
kubectl exec -it eks-iam-test2 -- ls /var/run/secrets/kubernetes.io/serviceaccount
kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token ;echo
# aws 서비스 사용 시도
kubectl exec -it eks-iam-test2 -- aws s3 ls
# 서비스 어카운트 토큰 확인
SA_TOKEN=$(kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $SA_TOKEN
https://jwt.io/에서 토큰 정보를 확인합니다.
기본 서비스 어카운트는 생성되어 파트에 Project로 Service Account의 토큰이 마운트 되었지만, 다시 Cloud Trail 로그를 확인해보면 아직 Access Denied입니다.
세 번째 파드를 생성해 봅니다.
아래 명령으로 IAM과 K8S 서비스어카운트를 매핑합니다.
# 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를 사용하는 Pod로 배포합니다.
# 파드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
aws s3 ls조회가 가능해 졌습니다.
Pod 내용을 보면 projected에 seviceAccount 부분이 변경된걸 확인할 수 있습니다.
kubectl get pod eks-iam-test3 -o yaml | kubectl neat | yh
위와같이 생성하면 AWS 콘솔 역할에서 아래 내용을 확인할 수 있습니다. OIDC주소는 Public이기 때문에 보안 위협이 될 수 있는데 Condition에 있는 서비스 어카운트를 *로 변경하면, Token이 발급되어 보안에 위협이 생길 수 있다고 합니다.
Pod Identity는 OIDC Provider를 개선한 방식입니다. Add-on을 추가하고 사용하기만 하면 됩니다.
Pod-Identity를 설치합니다.
aws eks create-addon --cluster-name $CLUSTER_NAME --addon-name eks-pod-identity-agent
# 확인
eksctl get addon --cluster $CLUSTER_NAME
해당 Add-on을 배포하면, eks-pod-identity-agent가 배포됩니다. 해당 파드는 워커노드의 호스트 네트워크를 사용하고, local에서만 접근 가능한 169.254.170.23주소에 80번 포트로 서비스를 게시하고, 파드들은 이쪽으로 credential 요청을 전송합니다.
이 기능을 사용하려면 노드그룹이 사용하는 역할에 AmazonEKSWorkerNodePolicy 정책 부분에 eks-auth 부분이 추가 되어 있어야 합니다.
podientity를 생성합니다.
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
파드를 배포합니다. s3 조회가 정상적으로 작동합니다.
# 서비스어카운트, 파드 생성
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
OWASP Kubernetes Top Ten
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
Web서버 배포
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"
접속 (admin/admin)
Create Database 클릭
재 로그인 (admin/password)
DVWA Security 수준 Low로 저장
Command Injection 공격 유형 |
사용자가 입력한 명령어 그대로 shell 스크립트를 실행할 수 있음을 확인했을 때
아래처럼 EC2의 인스턴스 프로필을 요청하는 방식으로 취약점이 생길 수 있다.
Kubelet 미흡한 인증/인가 설정 시 위험 |
(hakjunadmin@myeks:N/A) [root@myeks-bastion ~]# ssh ec2-user@$N1 sudo ss -tnlp | grep kubelet
LISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=3024,fd=16))
LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=3024,fd=13))
# 데모를 위해 awscli 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: myawscli
spec:
#serviceAccountName: my-sa
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
command: ['sleep', '36000']
restartPolicy: Never
terminationGracePeriodSeconds: 0
EOF
Bastion2에서 kubeletfct 설치 및 사용
# 기존 kubeconfig 삭제
rm -rf ~/.kube
# 다운로드
curl -LO https://github.com/cyberark/kubeletctl/releases/download/v1.11/kubeletctl_linux_amd64 && chmod a+x ./kubeletctl_linux_amd64 && mv ./kubeletctl_linux_amd64 /usr/local/bin/kubeletctl
kubeletctl version
kubeletctl help
# 노드1 IP 변수 지정
N1=<각자 자신의 노드1의 PrivateIP>
N1=192.168.1.206
# 노드1 IP로 Scan
kubeletctl scan --cidr $N1/32
# 노드1에 kubelet API 호출 시도
curl -k https://$N1:10250/pods; echo
Unauthorized
Node 1 접속 후 kubelet-config.json 수정
# 노드1 접속
ssh ec2-user@$N1
-----------------------------
# 미흡한 인증/인가 설정으로 변경
sudo vi /etc/kubernetes/kubelet/kubelet-config.json
...
"authentication": {
"anonymous": {
"enabled": true
...
},
"authorization": {
"mode": "AlwaysAllow",
...
# kubelet restart
sudo systemctl restart kubelet
systemctl status kubelet
-----------------------------
Bastion2에서 kubeletct 사용, 기존에 접근 안되던 부분들이 접근 가능해 짐.
# 파드 목록 확인
curl -s -k https://$N1:10250/pods | jq
# kubelet-config.json 설정 내용 확인
curl -k https://$N1:10250/configz | jq
# kubeletct 사용
# Return kubelet's configuration
kubeletctl -s $N1 configz | jq
# Get list of pods on the node
kubeletctl -s $N1 pods
# Scans for nodes with opened kubelet API > Scans for for all the tokens in a given Node
kubeletctl -s $N1 scan token
# 단, 아래 실습은 워커노드1에 myawscli 파드가 배포되어 있어야 실습이 가능. 물론 노드2~3에도 kubelet 수정하면 실습 가능함.
# kubelet API로 명령 실행 : <네임스페이스> / <파드명> / <컨테이너명>
curl -k https://$N1:10250/run/default/myawscli/my-aws-cli -d "cmd=aws --version"
# Scans for nodes with opened kubelet API > remote code execution on their containers
kubeletctl -s $N1 scan rce
# Run commands inside a container
kubeletctl -s $N1 exec "/bin/bash" -n default -p myawscli -c my-aws-cli
--------------------------------
export
aws --version
aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
exit
--------------------------------
# Return resource usage metrics (such as container CPU, memory usage, etc.)
kubeletctl -s $N1 metrics
Kyverno
Kubernetes Native Policy Management
Kyverno
Kyverno is a policy engine designed for Kubernetes
kyverno.io
동작 |
Mutating, Validating admission 과정에서 허용/차단 정책을 제공해 준다. kyverno는 rule의 집합으로 사용됨.
설치, EKS에서 사용 시 kube-system ns에 배포된 리소스는 대상에서 제외
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
kubectl get crd | grep kyverno
kubectl get pod,svc -n kyverno
# (참고) 기본 인증서 확인 https://kyverno.io/docs/installation/customization/#default-certificates
# step-cli 설치 https://smallstep.com/docs/step-cli/installation/
wget https://dl.smallstep.com/cli/docs-cli-install/latest/step-cli_amd64.rpm
sudo rpm -i step-cli_amd64.rpm
#
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
#
kubectl get validatingwebhookconfiguration kyverno-policy-validating-webhook-cfg -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d | step certificate inspect --short
프로메테우스 확인
그라파나 대시보드 확인 15804
cluster policy 생성, pod 라벨에 team이 있어야 함.
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
디플로이먼트 생성을 시도했으나, 생성 실패
key가 team인 label 추가 후 재배포 성공
validation 리포트 확인
Mutating |
파드를 대상으로 mutate 정책으로 라벨을 추가로 할당
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
파드 생성 시, team: bravo가 자동 추가됨
key에 team으로 지정하고 생성하면 value는 Override되어 들어간다.
Generation |
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
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
새로운 namespace 생성 후 secret을 확인해보면, regcred 시크릿이 자동으로 생성된 것을 확인할 수 있다.