[24단계 실습으로 정복하는 쿠버네티스] 책으로 스터디를 진행하였다.
쿠버네티스 보안
Cloud Native에서의 쿠버네티스 보안은 계층으로 크게 4C로 구분해서 제안하고 있다.
4C는 각각 클라우드(Cloud), 클러스터(Cluster), 컨테이너(Container)와 코드(Code)를 의미한다.
Cloud Provider(클라우드 공급자) 보안
쿠버네티스가 Cloud Native 구성에서 동작할 경우, 클라우드 계층에서의 보안이 취약하거나 취약한 방식으로 구성된다면 이는 곧 쿠버네티스 취약으로 이어진다.
Cloud Provider에서 제시하는 보안 권장사항을 참조하여 Cloud 보안 취약점을 제거해야 한다.
- AWS 보안서비스 : https://aws.amazon.com/security/
- Azure 보안서비스 : https://learn.microsoft.com/ko-kr/azure/security/fundamentals/overview
- GCP 보안서비스 : https://cloud.google.com/security/
인프라 보안을 위한 제안 목록은 아래와 같다.
- API 서버에 대한 네트워크 접근(컨트롤 플레인)
- 노드에 대한 네트워크 접근(노드)
- 클라우드 공급자 API에 대한 쿠버네티스 접근
- etcd에 대한 접근
- etcd 암호화
참고 : https://kubernetes.io/ko/docs/concepts/security/overview/#infrastructure-security
Cluster 보안
클러스터에서는 다른 리소스에서 클러스터에 접근할 때 필요한 최소 권한을 부여하는 식으로 보안 강화를 제안하고 있다.
- RBAC 인증(쿠버네티스 API에 대한 접근) : https://kubernetes.io/docs/reference/access-authn-authz/rbac
- 인증 : https://kubernetes.io/ko/docs/concepts/security/controlling-access
- 시크릿 관리 및 암호화
* https://kubernetes.io/ko/docs/concepts/configuration/secret
* https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data
- Pod의 보안정책 : https://kubernetes.io/docs/concepts/security/pod-security-standards/#policy-instantiation
- 서비스 품질(및 클러스터 리소스 관리) : https://kubernetes.io/ko/docs/tasks/configure-pod-container/quality-service-pod
- 네트워크 정책 : https://kubernetes.io/ko/docs/concepts/services-networking/network-policies
- Ingress의 TLS 통신 : https://kubernetes.io/ko/docs/concepts/services-networking/ingress/#tls
Container 보안
컨테이너 이미지의 취약점 및 컨테이너 접근에 대한 보안 강화를 제안하고 있다.
- 컨테이너 취약점 스캔 및 OS에 종속적인 보안
- 이미지 서명 및 시행
- 권한있는 사용자의 비허용
- 더 강력한 격리로 컨테이너 런타임 사용 : https://kubernetes.io/ko/docs/concepts/containers/runtime-class
Code(어플리케이션 소스코드) 보안
어플리케이션 소스코드에서 발생하는 취약점에 대한 보안강화를 제안하고 있다.
일반적으로 알려진 Secure Coding이나 SQL Injection 방지 등을 의미한다.
- TLS를 통한 접근
- 통신 포트 범위 제한
- 타사 종속성 보안
- 정적 코드 분석 : https://owasp.org/www-community/Source_Code_Analysis_Tools
- 동적 탐지 공격 : https://owasp.org/www-project-zap
이번 스터디에서는 위의 계층에서 몇가지 사례를 가지고 실습을 진행하였다.
EC2 Metadata 취약점 실습
Cloud Provider(클라우드 공급자) 보안의 하나로 AWS EC2 Metadata를 활용해 보안 취약점을 만들어 확인해보았다.
EC2 Metadata에 대한 확인
EC2 Metadata란, hostname, instance_id 등 EC2를 생성 또는 관리하는 데 사용될 수 있는 인스턴스 관련 데이터를 의미한다.
EC2를 AWS Console에서 기본 생성할 때 아래와 같은 설정화면을 통해 메타데이터 설정을 수행할 수 있다.
- 참고 : https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
당연히 이런 메타데이터도 구성에 대한 정보이므로, 조회하려면 IAM 권한이 필요하다.
IAM 권한이 있는 AccessKey로 AWS CLI를 통해 특정 인스턴스의 메타데이터를 조회하는 방법은 아래와 같다.
C:\Users\user>aws configure
AWS Access Key ID []: ********************
AWS Secret Access Key []: ****************************************
Default region name []: ap-northeast-2
Default output format []: json
C:\Users\user>aws ec2 describe-instances --region ap-northeast-2 --instance-id i-0920a8fb0b489738d --query "Reservations[0].Instances[0].MetadataOptions"
{
"State": "applied",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled",
"HttpProtocolIpv6": "disabled",
"InstanceMetadataTags": "disabled"
}
C:\Users\user>aws ec2 describe-instances --region ap-northeast-2 --instance-id i-0920a8fb0b489738d --query "Reservations[0].Instances[0].PublicIpAddress"
"52.78.25.129"
이 인스턴스 메타데이터 서비스(IMDS)는 통신할 때 버전1과 버전2로 나뉘는데,
버전1인 IMDSv1은 인스턴스 접근권한만 있으면 추가 권한 없이 메타데이터에 접근가능하고
버전2인 IMDSv2는 인스턴스 접근권한에 세션토큰을 추가로 요구한다.
사실 인스턴스 접근 권한만으로 충분한 인증을 한게 아닌가 싶어서, 좀 더 고민하면서 찾아보니
실제로 아래와 같은 서버 측 요청 위조(Server-Side Request Forgery, SSRF)공격을 통한 취약점 사례가 있었다.
- 출처 : https://sarangcho.co.kr/142
따라서 IMDSv1이 아닌 IMDSv2를 사용해야 한다.
다만, 위의 취약점을 걱정해 IMDS자체를 비활성화 하는 방안은 AWS 구성에 따라 고려해봐야할 사항이라고 생각한다.
AWS System Manager(SSM)이나 CloudWatch 등 다양한 서비스들이 IMDS를 통해서 기능을 제공하고 있기 때문에 연관된 서비스들이 IMDS를 사용하지 않는지 충분한 고려가 필요하다.
- SSM의 IMDS 사용 참고 : https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-technical-details.html
KOPS에서의 EC2 Metadata
AWS환경에서 KOPS를 활용해 구성된 K8S도 당연히 각 노드는 EC2 인스턴스이기 때문에 메타데이터를 제공하는데, 해당 설정은 KOPS의 instancegroup에서 정의된다.
(ersia:N/A) [root@kops-ec2 ~]# kops edit instancegroup nodes-ap-northeast-2a
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: kops.k8s.io/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: "2023-04-09T13:32:33Z"
labels:
kops.k8s.io/cluster: ersia.net
name: nodes-ap-northeast-2a
spec:
image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230302
############ 인스턴스 메타데이터 설정 ############
instanceMetadata:
httpPutResponseHopLimit: 1
httpTokens: required
#################################################
machineType: t3.medium
maxSize: 1
minSize: 1
role: Node
subnets:
- ap-northeast-2a
각 메타데이터 설정의 의미는 다음과 같다.
- httpTokens : required(IMDSv2만 사용)와 optional(IMDSv1과 IMDSv2를 요청에 따라 혼용)을 제공한다.
- httpPutResponseHopLimit : 일종의 TTL과 같은 개념으로 메타데이터 요청 패킷이 필요 이상의 다른 네트워크에 전파되지 않도록하는 설정이다.
현재 2개의 노드가 존재하는 상황에서 취약점 테스트를 위해 하나의 노드만 IMDSv2로 설정되어있는 메타데이터 설정을 IMDSv1으로 변경한다.
// 현재 구성의 Node확인
(ersia:N/A) [root@kops-ec2 ~]# kops get instancegroup
NAME ROLE MACHINETYPE MIN MAX ZONES
control-plane-ap-northeast-2a ControlPlane t3.medium 1 1 ap-northeast-2a
nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a
nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c
(ersia:N/A) [root@kops-ec2 ~]# kubectl get nodes -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
i-050d237cc2284528c Ready node 173m v1.24.12 172.30.65.57 13.125.252.158 Ubuntu 20.04.5 LTS 5.15.0-1031-aws containerd://1.6.18
i-08955414fcdb8715d Ready control-plane 176m v1.24.12 172.30.40.70 13.125.108.59 Ubuntu 20.04.5 LTS 5.15.0-1031-aws containerd://1.6.18
i-0b9bc5fbb89549907 Ready node 13m v1.24.12 172.30.52.228 3.36.105.149 Ubuntu 20.04.5 LTS 5.15.0-1031-aws containerd://1.6.18
// nodes-ap-northeast-2a 노드만 IMDSv1으로 변경
(ersia:N/A) [root@kops-ec2 ~]# kops edit instancegroup nodes-ap-northeast-2a
...
...
14 instanceMetadata:
15 httpPutResponseHopLimit: 1
16 httpTokens: optional ## 해당 부분을 변경
...
...
// 변경사항 적용 및 Rolling update 수행
(ersia:N/A) [root@kops-ec2 ~]# kops update cluster --yes
W0410 01:16:27.588168 9234 builder.go:230] failed to digest image "602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni:v1.12.2"
W0410 01:16:28.069504 9234 builder.go:230] failed to digest image "602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni-init:v1.12.2"
I0410 01:16:31.685081 9234 executor.go:111] Tasks: 0 done / 107 total; 52 can run
...
...
Cluster changes have been applied to the cloud.
(ersia:N/A) [root@kops-ec2 ~]# kops rolling-update cluster --yes
Detected single-control-plane cluster; won't detach before draining
NAME STATUS NEEDUPDATE READY MIN TARGET MAX NODES
control-plane-ap-northeast-2a Ready 0 1 1 1 1 1
nodes-ap-northeast-2a NeedsUpdate 1 0 1 1 1 1
nodes-ap-northeast-2c Ready 0 1 1 1 1 1
I0410 01:16:38.187844 9285 instancegroups.go:501] Validating the clus
...
...
I0410 01:20:17.607591 9285 instancegroups.go:537] Cluster validated.
I0410 01:20:17.607629 9285 rollingupdate.go:234] Rolling update completed for cluster "ersia.net"
테스트용 POD를 생성하고, 각 노드에 생성된 Pod에서 EC2 메타데이터를 조회한다.
인스턴스 메타데이터 서비스는 특정 IPv4 주소(169.254.169.254)를 통해서 메타데이터에 접근할 수 있다.
// 테스트용 netshoot POD 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 2
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
// POD 정보 확인
(ersia:N/A) [root@kops-ec2 ~]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
netshoot-pod-7757d5dd99-2h6fj 1/1 Running 0 70s 172.30.42.228 i-0b9bc5fbb89549907 <none> <none>
netshoot-pod-7757d5dd99-vbk7d 1/1 Running 0 70s 172.30.86.99 i-050d237cc2284528c <none> <none>
(ersia:N/A) [root@kops-ec2 ~]# A_POD=netshoot-pod-7757d5dd99-2h6fj
(ersia:N/A) [root@kops-ec2 ~]# C_POD=netshoot-pod-7757d5dd99-vbk7d
// POD에서 해당 노드에 부여된 IAM 정보를 조회할 수 있음
## POD A에서는 IMDSv1을 사용하기에 조회가 정상적으로 이루어짐
(ersia:N/A) [root@kops-ec2 ~]# kubectl exec -it $A_POD -- curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.ersia.net ;echo
{
"Code" : "Success",
"LastUpdated" : "2023-04-09T16:15:59Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "~~~HKBP",
"SecretAccessKey" : "~~~yTnl",
"Token" : "IQo~~~~~",
"Expiration" : "2023-04-09T22:51:48Z"
}
## POD C에서는 IMDSv2를 사용하므로 토큰정보가 없어 아무것도 출력하지 않음
(ersia:N/A) [root@kops-ec2 ~]# kubectl exec -it $C_POD -- curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.ersia.net ;echo
IMDSv1이 설정된 A노드의 A_POD에서 A노드에 적용된 IAM 프로필 정보가 확인되는 것을 볼 수 있다.
만약 임의의 POD가 외부에 노출되어 탈취되었을 경우,
이런식으로 획득한 AccessKey와 SecretKey를 가지고 부여된 IAM 역할의 모든 AWS 서비스를 사용할 수 있다.
AccessKey는 AWS CLI로 활용할 수도 있지만 이번 실습에서는 boto3로 AWS 서비스를 제어해봤다.
// boto3 python을 활용해 탈취한 AccessKey정보로 AWS 서비스 변경 시도
// boto3 pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: boto3-pod
spec:
replicas: 2
selector:
matchLabels:
app: boto3
template:
metadata:
labels:
app: boto3
spec:
containers:
- name: boto3
image: jpbarto/boto3
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
// boto3 pod 확인
(ersia:N/A) [root@kops-ec2 ~]# kubectl get pod -owide -l app=boto3
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
boto3-pod-7944d7b4db-px29g 1/1 Running 0 98s 172.30.42.229 i-0b9bc5fbb89549907 <none> <none>
boto3-pod-7944d7b4db-rlkwq 1/1 Running 0 98s 172.30.86.100 i-050d237cc2284528c <none> <none>
(ersia:N/A) [root@kops-ec2 ~]# A_BOTO3=boto3-pod-7944d7b4db-px29g
(ersia:N/A) [root@kops-ec2 ~]# C_BOTO3=boto3-pod-7944d7b4db-rlkwq
// A boto3 POD에서 boto3 python 스크립트 수행
// 기동중인 instacne정보를 받아와서 InstanceId 추출해서 출력
kubectl exec -it $A_BOTO3 -- sh
------------
cat <<EOF> ec2.py
import boto3
ec2 = boto3.resource('ec2', region_name='ap-northeast-2')
instances = ec2.instances.filter(
Filters=[{'Name': 'instance-state-name', 'Values': ['running']}])
for instance in instances:
print(instance.id, instance.instance_type)
EOF
python ec2.py;
exit
------------
// 출력 결과
~/dev #
~/dev # python ec2.py;
('i-050d237cc2284528c', 't3.medium')
('i-053faf154e228c666', 't3.small')
('i-08955414fcdb8715d', 't3.medium')
('i-0b9bc5fbb89549907', 't3.medium')
~/dev # exit
종료된 인스턴스 정보까지 포함해 instance_id가 모두 출력된 것을 확인할 수 있다.
만약 IAM역할에 EC2에 대한 모든 권한이 부여되었다면 EC2 생성, 삭제도 가능하지만,
부여된 IAM 권한 목록에서는 제한되어있다.
// 현재 탈취한 AccessKey의 IAM 권한 목록
{
"Statement": [
{
"Action": [
"s3:GetBucketLocation",
"s3:GetEncryptionConfiguration",
"s3:ListBucket",
"s3:ListBucketVersions"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::ersia-kops-s3"
]
},
{
"Action": [
"ec2:CreateTags"
],
"Effect": "Allow",
"Resource": [
"arn:aws:ec2:*:*:network-interface/*"
]
},
{
"Action": [
"autoscaling:DescribeAutoScalingInstances",
"ec2:AssignPrivateIpAddresses",
"ec2:AttachNetworkInterface",
"ec2:CreateNetworkInterface",
"ec2:DeleteNetworkInterface",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstances",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeRegions",
"ec2:DescribeTags",
"ec2:DetachNetworkInterface",
"ec2:ModifyNetworkInterfaceAttribute",
"ec2:UnassignPrivateIpAddresses",
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:DescribeRepositories",
"ecr:GetAuthorizationToken",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:ListImages",
"iam:GetServerCertificate",
"iam:ListServerCertificates",
"kms:GenerateRandom"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
}
부여된 권한에서 AssignPrivateIpAddresses를 활용해 보조IP를 추가할 수 있다.
// 특정 네트워크 인터페이스에 보조IP 추가
kubectl exec -it $A_BOTO3 -- sh
------------
cat <<EOF> ec2.py
import boto3
client = boto3.client('ec2', region_name='ap-northeast-2')
response = client.assign_private_ip_addresses(
AllowReassignment=False,
NetworkInterfaceId='eni-0b2ef535ecc2e859b',
PrivateIpAddresses=[
'10.0.0.111',
]
)
EOF
python ec2.py;
exit
------------
참고 : https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrationec2.html
이런 취약점을 예방하고 보안 강화를 위해서는
- EC2 Metadata 사용 시 IMDSv2를 사용해야 한다.
- EC2 노드에 부여되는 IAM에 필요 최소한의 권한만 부여한다.
또는 IRSA(IAM Role for Service Account)을 활용해 POD별 IAM 부여를 통해 권한을 분리해야 한다.
* 참고 : https://repost.aws/ko/knowledge-center/eks-restrict-s3-bucket
다행히 Kubernetes 1.27부터는 IMDSv2을 기본값으로 적용한다고 한다.
- 참고 : https://github.com/kubernetes/kops/blob/master/docs/instance_groups.md#instancemetadata
테스트 환경은 취약점 테스트를 위해 1.24에서 진행하였다.
(ersia:N/A) [root@kops-ec2 ~]# kubectl version --short
Flag --short has been deprecated, and will be removed in the future. The --short output will become the default.
Client Version: v1.26.3
Kustomize Version: v4.5.7
Server Version: v1.24.12
WARNING: version difference between client (1.26) and server (1.24) exceeds the supported minor version skew of +/-1
참고자료
- 클라우드 네이티브에서의 쿠버네티스 보안 개념 : https://kubernetes.io/ko/docs/concepts/security/overview
- 쿠버네티스 보안, 어떻게 해야 할까? : https://www.samsungsds.com/kr/insights/1258148_4627.html
- What makes IMDSv1 risky? : https://www.cloudyali.io/blogs/understanding-instance-metadata-service-imds
- [AWS] EC2 Metadata 관련하여 : https://bosungtea9416.tistory.com/entry/AWS-EC2-Metadata-관련하여