이번 게시글은 가시다님의 AEWS [2기] 스터디 내용을 정리한 포스트 입니다.
이번 게시글은 6 주차의 스터디 내용인 EKS Security에 대해 살펴봅니다.
EKS 배포
실습을 위해 EKS 클러스터를 배포합니다. AWS 콘솔에 로그인 한 뒤 CloudFormation에서 아래 YAML 파일로 스택을 생성하시면 됩니다. 진행 과정은 아래 링크를 참고해 주세요.
배포 확인
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'
`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
`kubectl rbac-tool whoami`는 현재 사용자 정보에 대해 출력합니다.
`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
`kubectl rbac-view`는 RBAC 권한을 웹 페이지로 시각화하여 제공합니다.
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
동작 |
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 시크릿이 자동으로 생성된 것을 확인할 수 있다.